diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index aa803f6..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "JetKVM", - "image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm", - "features": { - "ghcr.io/devcontainers/features/node:1": { - // Should match what is defined in ui/package.json - "version": "22.15.0" - } - }, - "mounts": [ - "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" - ], - "customizations": { - "vscode": { - "extensions": [ - "bradlc.vscode-tailwindcss", - "GitHub.vscode-pull-request-github", - "dbaeumer.vscode-eslint", - "golang.go", - "ms-vscode.makefile-tools", - "esbenp.prettier-vscode", - "github.vscode-github-actions" - ] - } - } -} - diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c7cbb22..fdeb5dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: jetkvm-app + name: kvm-app path: | - bin/jetkvm_app + bin/kvm_app device-tests.tar.gz \ No newline at end of file diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index ebce418..5e61e5a 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -42,13 +42,13 @@ jobs: sudo ip r r $CI_HOST via $CI_WG_GATEWAY dev wg-ci ping -c1 $CI_HOST || (echo "Failed to ping $CI_HOST" && sudo wg show wg-ci && ip r && exit 1) env: - CI_HOST: ${{ vars.JETKVM_CI_HOST }} - CI_WG_IPS: ${{ vars.JETKVM_CI_WG_IPS }} - CI_WG_GATEWAY: ${{ vars.JETKVM_CI_GATEWAY }} - CI_WG_ALLOWED_IPS: ${{ vars.JETKVM_CI_WG_ALLOWED_IPS }} - CI_WG_PUBLIC: ${{ secrets.JETKVM_CI_WG_PUBLIC }} - CI_WG_PRIVATE: ${{ secrets.JETKVM_CI_WG_PRIVATE }} - CI_WG_ENDPOINT: ${{ secrets.JETKVM_CI_WG_ENDPOINT }} + CI_HOST: ${{ vars.KVM_CI_HOST }} + CI_WG_IPS: ${{ vars.KVM_CI_WG_IPS }} + CI_WG_GATEWAY: ${{ vars.KVM_CI_GATEWAY }} + CI_WG_ALLOWED_IPS: ${{ vars.KVM_CI_WG_ALLOWED_IPS }} + CI_WG_PUBLIC: ${{ secrets.KVM_CI_WG_PUBLIC }} + CI_WG_PRIVATE: ${{ secrets.KVM_CI_WG_PRIVATE }} + CI_WG_ENDPOINT: ${{ secrets.KVM_CI_WG_ENDPOINT }} - name: Configure SSH run: | # Write SSH private key to a file @@ -66,9 +66,9 @@ jobs: IdentityFile $SSH_PRIVATE_KEY EOF env: - CI_USER: ${{ vars.JETKVM_CI_USER }} - CI_HOST: ${{ vars.JETKVM_CI_HOST }} - CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }} + CI_USER: ${{ vars.KVM_CI_USER }} + CI_HOST: ${{ vars.KVM_CI_HOST }} + CI_SSH_PRIVATE: ${{ secrets.KVM_CI_SSH_PRIVATE }} - name: Run tests run: | set -e @@ -116,12 +116,12 @@ jobs: set -e # Copy the binary 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 bin/kvm_app | gzip | ssh jkci "cat > /userdata/picokvm/kvm_app.update.gz" # Deploy and run the application on the remote host echo "+ Deploying the application on the remote host" ssh jkci ash </dev/null; then - echo "JetKVM is back online" + echo "KVM is back online" return 0 fi echo -n "." sleep 1 done - echo "JetKVM did not come back online within 60 seconds" + echo "KVM did not come back online within 60 seconds" return 1 } check_online env: - CI_HOST: ${{ vars.JETKVM_CI_HOST }} + CI_HOST: ${{ vars.KVM_CI_HOST }} - name: Run smoke tests run: | echo "+ Checking the status of the device" @@ -157,13 +157,13 @@ jobs: ssh jkci ash > $local_log_tar <<'EOF' log_path=$(mktemp -d) dmesg > $log_path/dmesg.log - cp /userdata/jetkvm/last.log $log_path/last.log + cp /userdata/picokvm/last.log $log_path/last.log tar -czf - -C $log_path . EOF tar -xf $local_log_tar cat dmesg.log last.log env: - CI_HOST: ${{ vars.JETKVM_CI_HOST }} + CI_HOST: ${{ vars.KVM_CI_HOST }} - name: Upload logs uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index f37d922..b402335 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ bin/* static/* .idea .DS_Store +.vscode device-tests.tar.gz \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index a553d21..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,130 +0,0 @@ -CODE_OF_CONDUCT.md - -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or - advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email - address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -support@jetkvm.com. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. diff --git a/Makefile b/Makefile index 2f3c74a..a814973 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,11 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) BUILDDATE ?= $(shell date -u +%FT%T%z) BUILDTS ?= $(shell date -u +%s) REVISION ?= $(shell git rev-parse HEAD) -VERSION_DEV ?= 0.4.5-dev$(shell date +%Y%m%d%H%M) -VERSION ?= 0.4.4 +VERSION_DEV ?= 0.0.1-dev +VERSION ?= 0.0.1 PROMETHEUS_TAG := github.com/prometheus/common/version -KVM_PKG_NAME := github.com/jetkvm/kvm +KVM_PKG_NAME := kvm GO_BUILD_ARGS := -tags netgo GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS) @@ -22,68 +22,19 @@ BIN_DIR := $(shell pwd)/bin TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u) -hash_resource: - @shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256 - -build_dev: hash_resource +build_dev: @echo "Building..." $(GO_CMD) build \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ $(GO_RELEASE_BUILD_ARGS) \ - -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)" \ - $(GO_BUILD_ARGS) \ - -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 . + -o $(BIN_DIR)/kvm_app cmd/main.go frontend: cd ui && npm ci && npm run build:device -dev_release: frontend build_dev - @echo "Uploading release..." - @shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1 > bin/jetkvm_app.sha256 - rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION_DEV)/jetkvm_app - rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION_DEV)/jetkvm_app.sha256 - -build_release: frontend hash_resource +build_release: frontend @echo "Building release..." $(GO_CMD) build \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \ $(GO_RELEASE_BUILD_ARGS) \ - -o bin/jetkvm_app cmd/main.go - -release: - @if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \ - echo "Error: Version $(VERSION) already exists. Please update the VERSION variable."; \ - exit 1; \ - fi - make build_release - @echo "Uploading release..." - @shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1 > bin/jetkvm_app.sha256 - rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION)/jetkvm_app - rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 + -o bin/kvm_app cmd/main.go diff --git a/README.md b/README.md deleted file mode 100644 index 0f0700f..0000000 --- a/README.md +++ /dev/null @@ -1,48 +0,0 @@ -
- JetKVM logo - -### KVM - -[Discord](https://jetkvm.com/discord) | [Website](https://jetkvm.com) | [Issues](https://github.com/jetkvm/cloud-api/issues) | [Docs](https://jetkvm.com/docs) - -[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/jetkvm.svg?style=social&label=Follow%20%40JetKVM)](https://twitter.com/jetkvm) - -[![Go Report Card](https://goreportcard.com/badge/github.com/jetkvm/kvm)](https://goreportcard.com/report/github.com/jetkvm/kvm) - -
- -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. - -## Features - -- **Ultra-low Latency** - 1080p@60FPS video with 30-60ms latency using H.264 encoding. Smooth mouse and keyboard interaction for responsive remote control. -- **Free & Optional Remote Access** - Remote management via JetKVM Cloud using WebRTC. -- **Open-source software** - Written in Golang on Linux. Easily customizable through SSH access to the JetKVM device. - -## Contributing - -We welcome contributions from the community! Whether it's improving the firmware, adding new features, or enhancing documentation, your input is valuable. We also have some rules and taboos here, so please read this page and our [Code of Conduct](/CODE_OF_CONDUCT.md) carefully. - -## I need help - -The best place to search for answers is our [Documentation](https://jetkvm.com/docs). If you can't find the answer there, check our [Discord Server](https://jetkvm.com/discord). - -## I want to report an issue - -If you've found an issue and want to report it, please check our [Issues](https://github.com/jetkvm/kvm/issues) page. Make sure the description contains information about the firmware version you're using, your platform, and a clear explanation of the steps to reproduce the issue. - -# Development - -JetKVM is written in Go & TypeScript. with some bits and pieces written in C. An intermediate level of Go & TypeScript knowledge is recommended for comfortable programming. - -The project contains two main parts, the backend software that runs on the KVM device and the frontend software that is served by the KVM device, and also the cloud. - -For most of local device development, all you need is to use the `./dev_deploy.sh` script. It will build the frontend and backend and deploy them to the local KVM device. Run `./dev_deploy.sh --help` for more information. - -## Backend - -The backend is written in Go and is responsible for the KVM device management, the cloud API and the cloud web. - -## Frontend - -The frontend is written in React and TypeScript and is served by the KVM device. It has three build targets: `device`, `development` and `production`. Development is used for development of the cloud version on your local machine, device is used for building the frontend for the KVM device and production is used for building the frontend for the cloud. diff --git a/audio.go b/audio.go new file mode 100644 index 0000000..0bf0ae5 --- /dev/null +++ b/audio.go @@ -0,0 +1,170 @@ +package kvm + +import ( + "encoding/json" + "fmt" + "net" + "sync" + "time" + + "github.com/pion/rtp" +) + +var ( + audioListener *net.UDPConn + currentPort int + mutex sync.Mutex + portList = []int{3333} + portIndex = 0 +) + +const ( + maxAudioFrameSize = 1500 + frameDurationMs = 20 + timestampRate = 48000 + timestampStep = timestampRate * frameDurationMs / 1000 +) + +func waitUDPPortReleased(port int, retries int, delay time.Duration) error { + addr := fmt.Sprintf("127.0.0.1:%d", port) + for i := 0; i < retries; i++ { + conn, err := net.ListenPacket("udp", addr) + if err == nil { + conn.Close() + return nil + } + time.Sleep(delay) + } + return fmt.Errorf("port %d still in use after %d retries", port, retries) +} + +func getNextAvailablePort() int { + for i := 0; i < len(portList); i++ { + port := portList[portIndex] + portIndex = (portIndex + 1) % len(portList) + addr := fmt.Sprintf("127.0.0.1:%d", port) + conn, err := net.ListenPacket("udp", addr) + if err == nil { + conn.Close() + return port + } + } + return 0 +} + +func StartNtpAudioServer(handleClient func(net.Conn)) { + mutex.Lock() + defer mutex.Unlock() + + if audioListener != nil || lastAudioState.Ready { + StopNtpAudioServer() + } + + port := getNextAvailablePort() + if port == 0 { + audioLogger.Error().Msg("no available ports to start audio server") + return + } + + listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: port}) + if err != nil { + audioLogger.Error().Err(err).Msg("failed to start server on port %d") + return + } + + audioListener = listener + currentPort = port + + if config.AudioMode == "usb" { + _, err := CallAudioCtrlAction("set_audio_mode", map[string]interface{}{"audio_mode": "usb", "rtp_port": port}) + if err != nil { + audioLogger.Error().Err(err).Msg("failed to set audio mode") + } + _, err = CallAudioCtrlAction("set_audio_enable", map[string]interface{}{"audio_enable": true}) + if err != nil { + audioLogger.Error().Err(err).Msg("failed to set audio enable") + } + } else { + _, err := CallAudioCtrlAction("set_audio_mode", map[string]interface{}{"audio_mode": "hdmi", "rtp_port": port}) + if err != nil { + audioLogger.Error().Err(err).Msg("failed to set audio mode") + } + _, err = CallAudioCtrlAction("set_audio_enable", map[string]interface{}{"audio_enable": true}) + if err != nil { + audioLogger.Error().Err(err).Msg("failed to set audio enable") + } + } + + go handleClient(listener) +} + +func StopNtpAudioServer() { + CallAudioCtrlAction("set_audio_enable", map[string]interface{}{"audio_enable": false}) + + if audioListener != nil { + audioListener.Close() + audioListener = nil + } + + if currentPort != 0 { + if err := waitUDPPortReleased(currentPort, 10, 200*time.Millisecond); err != nil { + audioLogger.Error().Err(err).Msg("port not released") + } + currentPort = 0 + } + + audioLogger.Info().Msg("audio server stopped") +} + +func handleAudioClient(conn net.Conn) { + defer conn.Close() + + audioLogger.Info().Msg("native audio socket client connected") + inboundPacket := make([]byte, maxAudioFrameSize) + var timestamp uint32 + var packet rtp.Packet + + for { + n, err := conn.Read(inboundPacket) + if err != nil { + audioLogger.Warn().Err(err).Msg("error during read") + return + } + + if currentSession != nil { + if err := packet.Unmarshal(inboundPacket[:n]); err != nil { + audioLogger.Warn().Err(err).Msg("error unmarshalling audio socket packet") + continue + } + + timestamp += timestampStep + packet.Header.Timestamp = timestamp + buf, err := packet.Marshal() + if err != nil { + audioLogger.Warn().Err(err).Msg("error marshalling packet") + continue + } + + if _, err := currentSession.AudioTrack.Write(buf); err != nil { + audioLogger.Warn().Err(err).Msg("error writing sample") + } + } + } +} + +type AudioInputState struct { + Ready bool `json:"ready"` + Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range +} + +var lastAudioState AudioInputState + +func HandleAudioStateMessage(event CtrlResponse) { + audioState := AudioInputState{} + err := json.Unmarshal(event.Data, &audioState) + if err != nil { + audioLogger.Warn().Err(err).Msg("Error parsing audio state json") + return + } + lastAudioState = audioState +} diff --git a/block_device.go b/block_device.go index 2274098..1a6b0b7 100644 --- a/block_device.go +++ b/block_device.go @@ -133,7 +133,7 @@ func (d *NBDDevice) runServerConn() { d.serverConn, []*server.Export{ { - Name: "jetkvm", + Name: "kvm", Description: "", Backend: &remoteImageBackend{}, }, diff --git a/block_device_linux.go b/block_device_linux.go index 8ca9372..80f0dcd 100644 --- a/block_device_linux.go +++ b/block_device_linux.go @@ -8,7 +8,7 @@ import ( func (d *NBDDevice) runClientConn() { err := client.Connect(d.clientConn, d.dev, &client.Options{ - ExportName: "jetkvm", + ExportName: "kvm", BlockSize: uint32(4 * 1024), }) d.l.Info().Err(err).Msg("nbd client exited") diff --git a/cloud.go b/cloud.go index cec749e..1087b55 100644 --- a/cloud.go +++ b/cloud.go @@ -1,23 +1,13 @@ package kvm import ( - "bytes" "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "sync" "time" "github.com/coder/websocket/wsjson" - "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/coreos/go-oidc/v3/oidc" - "github.com/coder/websocket" "github.com/gin-gonic/gin" "github.com/rs/zerolog" @@ -43,42 +33,30 @@ const ( ) var ( - metricCloudConnectionStatus = promauto.NewGauge( - prometheus.GaugeOpts{ - Name: "jetkvm_cloud_connection_status", - Help: "The status of the cloud connection", - }, - ) - metricCloudConnectionEstablishedTimestamp = promauto.NewGauge( - prometheus.GaugeOpts{ - Name: "jetkvm_cloud_connection_established_timestamp_seconds", - Help: "The timestamp when the cloud connection was established", - }, - ) metricConnectionLastPingTimestamp = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_connection_last_ping_timestamp_seconds", + Name: "kvm_connection_last_ping_timestamp_seconds", Help: "The timestamp when the last ping response was received", }, []string{"type", "source"}, ) metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_connection_last_ping_received_timestamp_seconds", + Name: "kvm_connection_last_ping_received_timestamp_seconds", Help: "The timestamp when the last ping request was received", }, []string{"type", "source"}, ) metricConnectionLastPingDuration = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_connection_last_ping_duration_seconds", + Name: "kvm_connection_last_ping_duration_seconds", Help: "The duration of the last ping response", }, []string{"type", "source"}, ) metricConnectionPingDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ - Name: "jetkvm_connection_ping_duration_seconds", + Name: "kvm_connection_ping_duration_seconds", Help: "The duration of the ping response", Buckets: []float64{ 0.1, 0.5, 1, 10, @@ -88,28 +66,28 @@ var ( ) metricConnectionTotalPingSentCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_connection_ping_sent_total", + Name: "kvm_connection_ping_sent_total", Help: "The total number of pings sent to the connection", }, []string{"type", "source"}, ) metricConnectionTotalPingReceivedCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_connection_ping_received_total", + Name: "kvm_connection_ping_received_total", Help: "The total number of pings received from the connection", }, []string{"type", "source"}, ) metricConnectionSessionRequestCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_connection_session_requests_total", + Name: "kvm_connection_session_requests_total", Help: "The total number of session requests received", }, []string{"type", "source"}, ) metricConnectionSessionRequestDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ - Name: "jetkvm_connection_session_request_duration_seconds", + Name: "kvm_connection_session_request_duration_seconds", Help: "The duration of session requests", Buckets: []float64{ 0.1, 0.5, 1, 10, @@ -119,320 +97,29 @@ var ( ) metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_connection_last_session_request_timestamp_seconds", + Name: "kvm_connection_last_session_request_timestamp_seconds", Help: "The timestamp of the last session request", }, []string{"type", "source"}, ) metricConnectionLastSessionRequestDuration = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_connection_last_session_request_duration", + Name: "kvm_connection_last_session_request_duration", Help: "The duration of the last session request", }, []string{"type", "source"}, ) - metricCloudConnectionFailureCount = promauto.NewCounter( - prometheus.CounterOpts{ - Name: "jetkvm_cloud_connection_failure_total", - Help: "The number of times the cloud connection has failed", - }, - ) ) -type CloudConnectionState uint8 - -const ( - CloudConnectionStateNotConfigured CloudConnectionState = iota - CloudConnectionStateDisconnected - CloudConnectionStateConnecting - CloudConnectionStateConnected -) - -var ( - cloudConnectionState CloudConnectionState = CloudConnectionStateNotConfigured - cloudConnectionStateLock = &sync.Mutex{} - - cloudDisconnectChan chan error - 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) { - metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1) - metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1) - - metricConnectionLastPingReceivedTimestamp.WithLabelValues(sourceType, source).Set(-1) - - metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).Set(-1) - metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(-1) - - if sourceType != "cloud" { - return - } - - if established { - metricCloudConnectionEstablishedTimestamp.SetToCurrentTime() - metricCloudConnectionStatus.Set(1) - } else { - metricCloudConnectionEstablishedTimestamp.Set(-1) - metricCloudConnectionStatus.Set(-1) - } -} - -func handleCloudRegister(c *gin.Context) { - var req CloudRegisterRequest - - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, gin.H{"error": "Invalid request body"}) - return - } - - // Exchange the temporary token for a permanent auth token - payload := struct { - TempToken string `json:"tempToken"` - }{ - TempToken: req.Token, - } - jsonPayload, err := json.Marshal(payload) - if err != nil { - c.JSON(500, gin.H{"error": "Failed to encode JSON payload: " + err.Error()}) - return - } - - client := &http.Client{Timeout: CloudAPIRequestTimeout} - - apiReq, err := http.NewRequest(http.MethodPost, config.CloudURL+"/devices/token", bytes.NewBuffer(jsonPayload)) - if err != nil { - c.JSON(500, gin.H{"error": "Failed to create register request: " + err.Error()}) - return - } - apiReq.Header.Set("Content-Type", "application/json") - - apiResp, err := client.Do(apiReq) - if err != nil { - c.JSON(500, gin.H{"error": "Failed to exchange token: " + err.Error()}) - return - } - defer apiResp.Body.Close() - - if apiResp.StatusCode != http.StatusOK { - c.JSON(apiResp.StatusCode, gin.H{"error": "Failed to exchange token: " + apiResp.Status}) - return - } - - var tokenResp struct { - SecretToken string `json:"secretToken"` - } - if err := json.NewDecoder(apiResp.Body).Decode(&tokenResp); err != nil { - c.JSON(500, gin.H{"error": "Failed to parse token response: " + err.Error()}) - return - } - - if tokenResp.SecretToken == "" { - c.JSON(500, gin.H{"error": "Received empty secret token"}) - return - } - - config.CloudToken = tokenResp.SecretToken - - provider, err := oidc.NewProvider(c, "https://accounts.google.com") - if err != nil { - c.JSON(500, gin.H{"error": "Failed to initialize OIDC provider: " + err.Error()}) - return - } - - oidcConfig := &oidc.Config{ - ClientID: req.ClientId, - } - - verifier := provider.Verifier(oidcConfig) - idToken, err := verifier.Verify(c, req.OidcGoogle) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid OIDC token: " + err.Error()}) - return - } - - config.GoogleIdentity = idToken.Audience[0] + ":" + idToken.Subject - - // Save the updated configuration - if err := SaveConfig(); err != nil { - c.JSON(500, gin.H{"error": "Failed to save configuration"}) - return - } - - c.JSON(200, gin.H{"message": "Cloud registration successful"}) -} - -func disconnectCloud(reason error) { - cloudDisconnectLock.Lock() - defer cloudDisconnectLock.Unlock() - - if cloudDisconnectChan == nil { - cloudLogger.Trace().Msg("cloud disconnect channel is not set, no need to disconnect") - return - } - - // just in case the channel is closed, we don't want to panic - defer func() { - if r := recover(); r != nil { - cloudLogger.Warn().Interface("reason", r).Msg("cloud disconnect channel is closed, no need to disconnect") - } - }() - cloudDisconnectChan <- reason -} - -func runWebsocketClient() error { - if config.CloudToken == "" { - time.Sleep(5 * time.Second) - return fmt.Errorf("cloud token is not set") - } - - wsURL, err := url.Parse(config.CloudURL) - if err != nil { - return fmt.Errorf("failed to parse config.CloudURL: %w", err) - } - - if wsURL.Scheme == "http" { - wsURL.Scheme = "ws" - } else { - wsURL.Scheme = "wss" - } - - setCloudConnectionState(CloudConnectionStateConnecting) - - header := http.Header{} - header.Set("X-Device-ID", GetDeviceID()) - header.Set("X-App-Version", builtAppVersion) - header.Set("Authorization", "Bearer "+config.CloudToken) - dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout) - - l := websocketLogger.With(). - Str("source", wsURL.Host). - Str("sourceType", "cloud"). - Logger() - - scopedLogger := &l - - defer cancelDial() - c, resp, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ - HTTPHeader: header, - OnPingReceived: func(ctx context.Context, payload []byte) bool { - scopedLogger.Debug().Bytes("payload", payload).Int("length", len(payload)).Msg("ping frame received") - - metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc() - metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime() - - setCloudConnectionState(CloudConnectionStateConnected) - - 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 err != nil { - if errors.Is(err, context.Canceled) { - cloudLogger.Info().Msg("websocket connection canceled") - setCloudConnectionState(CloudConnectionStateDisconnected) - - return nil - } - return err - } - defer c.CloseNow() //nolint:errcheck - cloudLogger.Info(). - Str("url", wsURL.String()). - Str("connectionID", connectionId). - Msg("websocket connected") - - // set the metrics when we successfully connect to the cloud. - wsResetMetrics(true, "cloud", wsURL.Host) - - // we don't have a source for the cloud connection - return handleWebRTCSignalWsMessages(c, true, wsURL.Host, connectionId, scopedLogger) -} - -func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { - oidcCtx, cancelOIDC := context.WithTimeout(ctx, CloudOidcRequestTimeout) - defer cancelOIDC() - provider, err := oidc.NewProvider(oidcCtx, "https://accounts.google.com") - if err != nil { - _ = wsjson.Write(context.Background(), c, gin.H{ - "error": fmt.Sprintf("failed to initialize OIDC provider: %v", err), - }) - cloudLogger.Warn().Err(err).Msg("failed to initialize OIDC provider") - return err - } - - oidcConfig := &oidc.Config{ - SkipClientIDCheck: true, - } - - verifier := provider.Verifier(oidcConfig) - idToken, err := verifier.Verify(oidcCtx, req.OidcGoogle) - if err != nil { - return err - } - - googleIdentity := idToken.Audience[0] + ":" + idToken.Subject - if config.GoogleIdentity != googleIdentity { - _ = wsjson.Write(context.Background(), c, gin.H{"error": "google identity mismatch"}) - return fmt.Errorf("google identity mismatch") - } - - return nil -} - func handleSessionRequest( ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest, - isCloudConnection bool, source string, scopedLogger *zerolog.Logger, ) error { var sourceType string - if isCloudConnection { - sourceType = "cloud" - } else { - sourceType = "local" - } + sourceType = "local" timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(v) @@ -440,16 +127,8 @@ func handleSessionRequest( })) defer timer.ObserveDuration() - // If the message is from the cloud, we need to authenticate the session. - if isCloudConnection { - if err := authenticateSession(ctx, c, req); err != nil { - return err - } - } - session, err := newSession(SessionConfig{ ws: c, - IsCloud: isCloudConnection, LocalIP: req.IP, ICEServers: req.ICEServers, Logger: scopedLogger, @@ -479,90 +158,3 @@ func handleSessionRequest( _ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd}) return nil } - -func RunWebsocketClient() { - for { - // If the cloud token is not set, we don't need to run the websocket client. - if config.CloudToken == "" { - time.Sleep(5 * time.Second) - continue - } - - // If the network is not up, well, we can't connect to the cloud. - if !networkState.IsOnline() { - cloudLogger.Warn().Msg("waiting for network to be online, will retry in 3 seconds") - time.Sleep(3 * time.Second) - continue - } - - // If the system time is not synchronized, the API request will fail anyway because the TLS handshake will fail. - if isTimeSyncNeeded() && !timeSync.IsSyncSuccess() { - cloudLogger.Warn().Msg("system time is not synced, will retry in 3 seconds") - time.Sleep(3 * time.Second) - continue - } - - err := runWebsocketClient() - if err != nil { - cloudLogger.Warn().Err(err).Msg("websocket client error") - metricCloudConnectionStatus.Set(0) - metricCloudConnectionFailureCount.Inc() - time.Sleep(5 * time.Second) - } - } -} - -type CloudState struct { - Connected bool `json:"connected"` - URL string `json:"url,omitempty"` - AppURL string `json:"appUrl,omitempty"` -} - -func rpcGetCloudState() CloudState { - return CloudState{ - Connected: config.CloudToken != "" && config.CloudURL != "", - URL: config.CloudURL, - AppURL: config.CloudAppURL, - } -} - -func rpcDeregisterDevice() error { - if config.CloudToken == "" || config.CloudURL == "" { - return fmt.Errorf("cloud token or URL is not set") - } - - req, err := http.NewRequest(http.MethodDelete, config.CloudURL+"/devices/"+GetDeviceID(), nil) - if err != nil { - return fmt.Errorf("failed to create deregister request: %w", err) - } - - req.Header.Set("Authorization", "Bearer "+config.CloudToken) - client := &http.Client{Timeout: CloudAPIRequestTimeout} - resp, err := client.Do(req) - if err != nil { - return fmt.Errorf("failed to send deregister request: %w", err) - } - - defer resp.Body.Close() - // We consider both 200 OK and 404 Not Found as successful deregistration. - // 200 OK means the device was found and deregistered. - // 404 Not Found means the device is not in the database, which could be due to various reasons - // (e.g., wrong cloud token, already deregistered). Regardless of the reason, we can safely remove it. - if resp.StatusCode == http.StatusNotFound || (resp.StatusCode >= 200 && resp.StatusCode < 300) { - config.CloudToken = "" - config.GoogleIdentity = "" - - if err := SaveConfig(); err != nil { - return fmt.Errorf("failed to save configuration after deregistering: %w", err) - } - - cloudLogger.Info().Msg("device deregistered, disconnecting from cloud") - disconnectCloud(fmt.Errorf("device deregistered")) - - setCloudConnectionState(CloudConnectionStateNotConfigured) - - return nil - } - - return fmt.Errorf("deregister request failed with status: %s", resp.Status) -} diff --git a/cmd/main.go b/cmd/main.go index ab44ac9..6080aff 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/jetkvm/kvm" + "kvm" ) func main() { diff --git a/config.go b/config.go index 3e88457..71bcc4f 100644 --- a/config.go +++ b/config.go @@ -6,9 +6,9 @@ import ( "os" "sync" - "github.com/jetkvm/kvm/internal/logging" - "github.com/jetkvm/kvm/internal/network" - "github.com/jetkvm/kvm/internal/usbgadget" + "kvm/internal/logging" + "kvm/internal/network" + "kvm/internal/usbgadget" ) type WakeOnLanDevice struct { @@ -75,8 +75,6 @@ func (m *KeyboardMacro) Validate() error { } type Config struct { - CloudURL string `json:"cloud_url"` - CloudAppURL string `json:"cloud_app_url"` CloudToken string `json:"cloud_token"` GoogleIdentity string `json:"google_identity"` JigglerEnabled bool `json:"jiggler_enabled"` @@ -100,17 +98,26 @@ type Config struct { UsbDevices *usbgadget.Devices `json:"usb_devices"` NetworkConfig *network.NetworkConfig `json:"network_config"` DefaultLogLevel string `json:"default_log_level"` + TailScaleAutoStart bool `json:"tailscale_autostart"` + TailScaleXEdge bool `json:"tailscale_xedge"` + ZeroTierNetworkID string `json:"zerotier_network_id"` + ZeroTierAutoStart bool `json:"zerotier_autostart"` + IO0Status bool `json:"io0_status"` + IO1Status bool `json:"io1_status"` + AudioMode string `json:"audio_mode"` + TimeZone string `json:"time_zone"` + LEDGreenMode string `json:"led_green_mode"` + LEDYellowMode string `json:"led_yellow_mode"` } const configPath = "/userdata/kvm_config.json" var defaultConfig = &Config{ - CloudURL: "https://api.jetkvm.com", - CloudAppURL: "https://app.jetkvm.com", - AutoUpdateEnabled: true, // Set a default value + AutoUpdateEnabled: false, // Set a default value ActiveExtension: "", KeyboardMacros: []KeyboardMacro{}, - DisplayRotation: "270", + DisplayRotation: "180", + TimeZone: "UTC-8", KeyboardLayout: "en_US", DisplayMaxBrightness: 64, DisplayDimAfterSec: 120, // 2 minutes @@ -120,7 +127,7 @@ var defaultConfig = &Config{ VendorId: "0x1d6b", //The Linux Foundation ProductId: "0x0104", //Multifunction Composite Gadget SerialNumber: "", - Manufacturer: "JetKVM", + Manufacturer: "KVM", Product: "USB Emulation Device", }, UsbDevices: &usbgadget.Devices{ @@ -128,9 +135,18 @@ var defaultConfig = &Config{ RelativeMouse: true, Keyboard: true, MassStorage: true, + Audio: true, }, - NetworkConfig: &network.NetworkConfig{}, - DefaultLogLevel: "INFO", + NetworkConfig: &network.NetworkConfig{}, + DefaultLogLevel: "INFO", + ZeroTierAutoStart: false, + TailScaleAutoStart: false, + TailScaleXEdge: false, + IO0Status: true, + IO1Status: true, + AudioMode: "disabled", + LEDGreenMode: "network-rx", + LEDYellowMode: "kernel-activity", } var ( diff --git a/dev_deploy.sh b/dev_deploy.sh deleted file mode 100755 index 059e416..0000000 --- a/dev_deploy.sh +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env bash -# -# Exit immediately if a command exits with a non-zero status -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 -show_help() { - echo "Usage: $0 [options] -r " - echo - echo "Required:" - echo " -r, --remote Remote host IP address" - echo - echo "Optional:" - echo " -u, --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 " --help Display this help message" - echo - echo "Example:" - echo " $0 -r 192.168.0.17" - echo " $0 -r 192.168.0.17 -u admin" -} - -# Default values -REMOTE_USER="root" -REMOTE_PATH="/userdata/jetkvm/bin" -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 -while [[ $# -gt 0 ]]; do - case $1 in - -r|--remote) - REMOTE_HOST="$2" - shift 2 - ;; - -u|--user) - REMOTE_USER="$2" - shift 2 - ;; - --skip-ui-build) - SKIP_UI_BUILD=true - 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) - show_help - exit 0 - ;; - *) - echo "Unknown option: $1" - show_help - exit 1 - ;; - esac -done - -# Verify required parameters -if [ -z "$REMOTE_HOST" ]; then - msg_err "Error: Remote IP is a required parameter" - show_help - exit 1 -fi - -# Build the development version on the host -if [ "$SKIP_UI_BUILD" = false ]; then - msg_info "▶ Building frontend" - make frontend -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 - -# Kill any existing instances of the application -ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" - -# Copy the binary to the remote host -ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app - -if [ "$RESET_USB_HID_DEVICE" = true ]; then - msg_info "▶ Resetting USB HID device" - msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed" - # Remove the old USB gadget configuration - ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*" - ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC" -fi - -# Deploy and run the application on the remote host -ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF -set -e - -# Set the library path to include the directory where librockit.so is located -export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH - -# Kill any existing instances of the application -killall jetkvm_app || true -killall jetkvm_app_debug || true - -# Navigate to the directory where the binary will be stored -cd "${REMOTE_PATH}" - -# Make the new binary executable -chmod +x jetkvm_app_debug - -# Run the application in the background -PION_LOG_TRACE=${LOG_TRACE_SCOPES} GODEBUG=netdns=1 ./jetkvm_app_debug -EOF - -echo "Deployment complete." \ No newline at end of file diff --git a/display.go b/display.go index cf1a0cc..b43fe58 100644 --- a/display.go +++ b/display.go @@ -2,7 +2,6 @@ package kvm import ( "errors" - "fmt" "os" "strconv" "sync" @@ -18,12 +17,12 @@ var ( ) const ( - touchscreenDevice string = "/dev/input/event1" + touchscreenDevice string = "/dev/input/event0" backlightControlClass string = "/sys/class/backlight/backlight/brightness" ) func switchToScreen(screen string) { - _, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen}) + _, err := CallDisplayCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen}) if err != nil { displayLogger.Warn().Err(err).Str("screen", screen).Msg("failed to switch to screen") return @@ -34,15 +33,15 @@ func switchToScreen(screen 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}) + return CallDisplayCtrlAction("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}) + return CallDisplayCtrlAction("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}) + return CallDisplayCtrlAction("lv_obj_clear_flag", map[string]interface{}{"obj": objName, "flag": flag}) } func lvObjHide(objName string) (*CtrlResponse, error) { @@ -54,27 +53,31 @@ func lvObjShow(objName string) (*CtrlResponse, error) { } 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}) + return CallDisplayCtrlAction("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}) + return CallDisplayCtrlAction("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}) + return CallDisplayCtrlAction("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}) + return CallDisplayCtrlAction("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}) + return CallDisplayCtrlAction("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}) + return CallDisplayCtrlAction("lv_disp_set_rotation", map[string]interface{}{"rotation": rotation}) +} + +func lvObjSetStyleBgColor(objName string, color string) (*CtrlResponse, error) { + return CallDisplayCtrlAction("lv_obj_set_style_bg_color", map[string]interface{}{"obj": objName, "color": color}) } func updateLabelIfChanged(objName string, newText string) { @@ -98,83 +101,26 @@ var ( ) func updateDisplay() { - updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4String()) + updateLabelIfChanged("Network_Address_IP_Label", networkState.IPv4String()) + updateLabelIfChanged("Version_Hostname_Label", GetHostname()) + if usbState == "configured" { - updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Connected") - _, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_DEFAULT") + _, _ = lvObjSetState("Main", "USB_CONNECTED") } else { - updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Disconnected") - _, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_USER_2") + _, _ = lvObjSetState("Main", "USB_DISCONNECTED") } if lastVideoState.Ready { - updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Connected") - _, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_DEFAULT") + _, _ = lvObjSetState("Main", "HDMI_CONNECTED") } else { - updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Disconnected") - _, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_USER_2") + _, _ = lvObjSetState("Main", "HDMI_DISCONNECTED") } - updateLabelIfChanged("ui_Home_Header_Cloud_Status_Label", fmt.Sprintf("%d active", actionSessions)) if networkState.IsUp() { - switchToScreenIfDifferent("ui_Home_Screen") + _, _ = lvObjSetState("Network", "NETWORK") } else { - switchToScreenIfDifferent("ui_No_Network_Screen") + _, _ = lvObjSetState("Network", "NO_NETWORK") } - 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() { - 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 ( @@ -205,34 +151,31 @@ func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool) { waitDisplayUpdate.Lock() defer waitDisplayUpdate.Unlock() - waitCtrlClientConnected() + waitDisplayCtrlClientConnected() requestDisplayUpdate(shouldWakeDisplay) } func updateStaticContents() { //contents that never change - updateLabelIfChanged("ui_Home_Content_Mac", networkState.MACString()) - systemVersion, appVersion, err := GetLocalVersion() + updateLabelIfChanged("Network_Address_Mac_Label", networkState.MACString()) + _, appVersion, err := GetLocalVersion() if err == nil { - updateLabelIfChanged("ui_About_Content_Operating_System_Version_ContentLabel", systemVersion.String()) - updateLabelIfChanged("ui_About_Content_App_Version_Content_Label", appVersion.String()) + updateLabelIfChanged("Version_App_Version_Label", appVersion.String()) } - - updateLabelIfChanged("ui_Status_Content_Device_Id_Content_Label", GetDeviceID()) } // setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter -// the backlight brightness of the JetKVM hardware's display. +// the backlight brightness of the KVM hardware's display. func setDisplayBrightness(brightness int) error { // NOTE: The actual maximum value for this is 255, but out-of-the-box, the value is set to 64. // The maximum set here is set to 100 to reduce the risk of drawing too much power (and besides, 255 is very bright!). - if brightness > 100 || brightness < 0 { + if brightness > 200 || brightness < 0 { return errors.New("brightness value out of bounds, must be between 0 and 100") } // Check the display backlight class is available if _, err := os.Stat(backlightControlClass); errors.Is(err, os.ErrNotExist) { - return errors.New("brightness value cannot be set, possibly not running on JetKVM hardware") + return errors.New("brightness value cannot be set, possibly not running on KVM hardware") } // Set the value @@ -302,8 +245,6 @@ func wakeDisplay(force bool) { // watchTsEvents monitors the touchscreen for events and simply calls wakeDisplay() to ensure the // touchscreen interface still works even with LCD dimming/off. -// TODO: This is quite a hack, really we should be getting an event from jetkvm_native, or the whole display backlight -// control should be hoisted up to jetkvm_native. func watchTsEvents() { ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666) if err != nil { @@ -379,11 +320,12 @@ func startBacklightTickers() { func initDisplay() { go func() { - waitCtrlClientConnected() + waitDisplayCtrlClientConnected() displayLogger.Info().Msg("setting initial display contents") time.Sleep(500 * time.Millisecond) _, _ = lvDispSetRotation(config.DisplayRotation) updateStaticContents() + initTimeZone() displayInited = true displayLogger.Info().Msg("display inited") startBacklightTickers() diff --git a/go.mod b/go.mod index 3e38ac1..65c96f3 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/jetkvm/kvm +module kvm go 1.23.4 @@ -63,18 +63,26 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pilebones/go-udev v0.9.0 // indirect github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/dtls/v3 v3.0.6 // indirect + github.com/pion/ice/v2 v2.3.36 // indirect github.com/pion/ice/v4 v4.0.10 // indirect github.com/pion/interceptor v0.1.40 // indirect + github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/rtcp v1.2.15 // indirect github.com/pion/rtp v1.8.18 // indirect github.com/pion/sctp v1.8.39 // indirect github.com/pion/sdp/v3 v3.0.13 // indirect + github.com/pion/srtp/v2 v2.0.20 // indirect github.com/pion/srtp/v3 v3.0.5 // indirect + github.com/pion/stun v0.6.1 // indirect github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/turn/v2 v2.1.6 // indirect github.com/pion/turn/v4 v4.0.2 // indirect + github.com/pion/webrtc/v3 v3.3.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect diff --git a/go.sum b/go.sum index 6e7e5d5..63bdc28 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,7 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 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/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ= @@ -70,6 +71,7 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -103,34 +105,59 @@ github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= +github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc= +github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= +github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= +github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU= github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= +github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo= github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= +github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= +github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= +github.com/pion/webrtc/v3 v3.3.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg= +github.com/pion/webrtc/v3 v3.3.5/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= github.com/pion/webrtc/v4 v4.0.16 h1:5f8QMVIbNvJr2mPRGi2QamkPa/LVUB6NWolOCwphKHA= github.com/pion/webrtc/v4 v4.0.16/go.mod h1:C3uTCPzVafUA0eUzru9f47OgNt3nEO7ZJ6zNY6VSJno= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -161,6 +188,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -171,30 +200,81 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= 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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220722155257-8c9f86f7a55f/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 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 v1.0.0-20190902080502-41f04d3bba15/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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hw.go b/hw.go index 20d88eb..8819ef7 100644 --- a/hw.go +++ b/hw.go @@ -53,12 +53,22 @@ func GetDeviceID() string { } func GetDefaultHostname() string { - deviceId := GetDeviceID() - if deviceId == "unknown_device_id" { - return "jetkvm" + //deviceId := GetDeviceID() + //if deviceId == "unknown_device_id" { + // return "kvm" + //} + + //return fmt.Sprintf("kvm-%s", strings.ToLower(deviceId)) + return "picokvm" +} + +func GetHostname() string { + content, err := os.ReadFile("/etc/hostname") + if err != nil { + return GetDefaultHostname() } - return fmt.Sprintf("jetkvm-%s", strings.ToLower(deviceId)) + return strings.TrimSpace(string(content)) } func runWatchdog() { diff --git a/internal/logging/logger.go b/internal/logging/logger.go index 39156ec..57978d8 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -97,7 +97,7 @@ func (l *Logger) updateLogLevel() { finalDefaultLogLevel := l.defaultLogLevel for name, level := range zerologLevels { - env := os.Getenv(fmt.Sprintf("JETKVM_LOG_%s", name)) + env := os.Getenv(fmt.Sprintf("KVM_LOG_%s", name)) if env == "" { env = os.Getenv(fmt.Sprintf("PION_LOG_%s", name)) diff --git a/internal/logging/pion.go b/internal/logging/pion.go index 453b8bc..7eaac06 100644 --- a/internal/logging/pion.go +++ b/internal/logging/pion.go @@ -48,10 +48,17 @@ func (c pionLogger) Errorf(format string, args ...interface{}) { type pionLoggerFactory struct{} func (c pionLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger { - logger := rootLogger.getLogger(subsystem).With(). + var logger zerolog.Logger + + base := rootLogger.getLogger(subsystem).With(). Str("scope", "pion"). - Str("component", subsystem). - Logger() + Str("component", subsystem) + + if subsystem == "mdns" { + logger = base.Logger().Level(zerolog.ErrorLevel) // 或 ErrorLevel + } else { + logger = base.Logger() + } return pionLogger{logger: &logger} } diff --git a/internal/logging/root.go b/internal/logging/root.go index 397ca64..d69f676 100644 --- a/internal/logging/root.go +++ b/internal/logging/root.go @@ -4,7 +4,7 @@ import "github.com/rs/zerolog" var ( rootZerologLogger = zerolog.New(defaultLogOutput).With(). - Str("scope", "jetkvm"). + Str("scope", "kvm"). Timestamp(). Stack(). Logger() diff --git a/internal/mdns/mdns.go b/internal/mdns/mdns.go index b882b93..204f9fe 100644 --- a/internal/mdns/mdns.go +++ b/internal/mdns/mdns.go @@ -7,7 +7,8 @@ import ( "strings" "sync" - "github.com/jetkvm/kvm/internal/logging" + "kvm/internal/logging" + pion_mdns "github.com/pion/mdns/v2" "github.com/rs/zerolog" "golang.org/x/net/ipv4" diff --git a/internal/network/config.go b/internal/network/config.go index 74ddf19..7fbbab8 100644 --- a/internal/network/config.go +++ b/internal/network/config.go @@ -5,8 +5,9 @@ import ( "net" "time" + "kvm/internal/mdns" + "github.com/guregu/null/v6" - "github.com/jetkvm/kvm/internal/mdns" "golang.org/x/net/idna" ) diff --git a/internal/network/netif.go b/internal/network/netif.go index c5db806..bcba5dc 100644 --- a/internal/network/netif.go +++ b/internal/network/netif.go @@ -5,9 +5,10 @@ import ( "net" "sync" - "github.com/jetkvm/kvm/internal/confparser" - "github.com/jetkvm/kvm/internal/logging" - "github.com/jetkvm/kvm/internal/udhcpc" + "kvm/internal/confparser" + "kvm/internal/logging" + "kvm/internal/udhcpc" + "github.com/rs/zerolog" "github.com/vishvananda/netlink" @@ -58,7 +59,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS } if opts.DefaultHostname == "" { - opts.DefaultHostname = "jetkvm" + opts.DefaultHostname = "picokvm" } err := confparser.SetDefaultsAndValidate(opts.NetworkConfig) diff --git a/internal/network/rpc.go b/internal/network/rpc.go index 32f34f5..ef74b9c 100644 --- a/internal/network/rpc.go +++ b/internal/network/rpc.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/jetkvm/kvm/internal/confparser" - "github.com/jetkvm/kvm/internal/udhcpc" + "kvm/internal/confparser" + "kvm/internal/udhcpc" ) type RpcIPv6Address struct { diff --git a/internal/timesync/metrics.go b/internal/timesync/metrics.go index 5aa2e92..cc07e4b 100644 --- a/internal/timesync/metrics.go +++ b/internal/timesync/metrics.go @@ -8,64 +8,64 @@ import ( var ( metricTimeSyncStatus = promauto.NewGauge( prometheus.GaugeOpts{ - Name: "jetkvm_timesync_status", + Name: "kvm_timesync_status", Help: "The status of the timesync, 1 if successful, 0 if not", }, ) metricTimeSyncCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_total", + Name: "kvm_timesync_total", Help: "The number of times the timesync has been run", }, ) metricTimeSyncSuccessCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_success_total", + Name: "kvm_timesync_success_total", Help: "The number of times the timesync has been successful", }, ) metricRTCUpdateCount = promauto.NewCounter( //nolint:unused prometheus.CounterOpts{ - Name: "jetkvm_timesync_rtc_update_total", + Name: "kvm_timesync_rtc_update_total", Help: "The number of times the RTC has been updated", }, ) metricNtpTotalSuccessCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_ntp_total_success_total", + Name: "kvm_timesync_ntp_total_success_total", Help: "The total number of successful NTP requests", }, ) metricNtpTotalRequestCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_ntp_total_request_total", + Name: "kvm_timesync_ntp_total_request_total", Help: "The total number of NTP requests sent", }, ) metricNtpSuccessCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_timesync_ntp_success_total", + Name: "kvm_timesync_ntp_success_total", Help: "The number of successful NTP requests", }, []string{"url"}, ) metricNtpRequestCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_timesync_ntp_request_total", + Name: "kvm_timesync_ntp_request_total", Help: "The number of NTP requests sent to the server", }, []string{"url"}, ) metricNtpServerLastRTT = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_timesync_ntp_server_last_rtt", + Name: "kvm_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", + Name: "kvm_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, @@ -75,7 +75,7 @@ var ( ) metricNtpServerInfo = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_timesync_ntp_server_info", + Name: "kvm_timesync_ntp_server_info", Help: "The info of the NTP server", }, []string{"url", "reference", "stratum", "precision"}, @@ -83,53 +83,53 @@ var ( metricHttpTotalSuccessCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_total_success_total", + Name: "kvm_timesync_http_total_success_total", Help: "The total number of successful HTTP requests", }, ) metricHttpTotalRequestCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_total_request_total", + Name: "kvm_timesync_http_total_request_total", Help: "The total number of HTTP requests sent", }, ) metricHttpTotalCancelCount = promauto.NewCounter( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_total_cancel_total", + Name: "kvm_timesync_http_total_cancel_total", Help: "The total number of HTTP requests cancelled", }, ) metricHttpSuccessCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_success_total", + Name: "kvm_timesync_http_success_total", Help: "The number of successful HTTP requests", }, []string{"url"}, ) metricHttpRequestCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_request_total", + Name: "kvm_timesync_http_request_total", Help: "The number of HTTP requests sent to the server", }, []string{"url"}, ) metricHttpCancelCount = promauto.NewCounterVec( prometheus.CounterOpts{ - Name: "jetkvm_timesync_http_cancel_total", + Name: "kvm_timesync_http_cancel_total", Help: "The number of HTTP requests cancelled", }, []string{"url"}, ) metricHttpServerLastRTT = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_timesync_http_server_last_rtt", + Name: "kvm_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", + Name: "kvm_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, @@ -139,7 +139,7 @@ var ( ) metricHttpServerInfo = promauto.NewGaugeVec( prometheus.GaugeOpts{ - Name: "jetkvm_timesync_http_server_info", + Name: "kvm_timesync_http_server_info", Help: "The info of the HTTP server", }, []string{"url", "http_code"}, diff --git a/internal/timesync/timesync.go b/internal/timesync/timesync.go index e956cf9..15fcf82 100644 --- a/internal/timesync/timesync.go +++ b/internal/timesync/timesync.go @@ -7,7 +7,8 @@ import ( "sync" "time" - "github.com/jetkvm/kvm/internal/network" + "kvm/internal/network" + "github.com/rs/zerolog" ) diff --git a/internal/udhcpc/udhcpc.go b/internal/udhcpc/udhcpc.go index 128ea66..911ee28 100644 --- a/internal/udhcpc/udhcpc.go +++ b/internal/udhcpc/udhcpc.go @@ -4,12 +4,15 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" "reflect" "time" "github.com/fsnotify/fsnotify" "github.com/rs/zerolog" + "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" ) const ( @@ -66,6 +69,36 @@ func (c *DHCPClient) getWatchPaths() []string { return paths } +func (c *DHCPClient) watchLink() { + ch := make(chan netlink.LinkUpdate) + done := make(chan struct{}) + + if err := netlink.LinkSubscribe(ch, done); err != nil { + c.logger.Error().Err(err).Msg("failed to subscribe to netlink") + return + } + + for update := range ch { + if update.Link.Attrs().Name == c.InterfaceName { + if update.IfInfomsg.Flags&unix.IFF_RUNNING != 0 { + fmt.Printf("[watchLink]link is up, starting udhcpc") + go c.runUDHCPC() + } else { + c.logger.Info().Msg("link is down") + } + } + } +} + +func (c *DHCPClient) runUDHCPC() { + cmd := exec.Command("udhcpc", "-i", c.InterfaceName, "-t", "1") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + c.logger.Error().Err(err).Msg("failed to run udhcpc") + } +} + // 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 { @@ -80,6 +113,8 @@ func (c *DHCPClient) Run() error { } defer watcher.Close() + go c.watchLink() + go func() { for { select { diff --git a/internal/usbgadget/changeset_arm_test.go b/internal/usbgadget/changeset_arm_test.go index 8c0abd5..cdb4076 100644 --- a/internal/usbgadget/changeset_arm_test.go +++ b/internal/usbgadget/changeset_arm_test.go @@ -15,7 +15,7 @@ var ( VendorId: "0x1d6b", //The Linux Foundation ProductId: "0x0104", //Multifunction Composite Gadget SerialNumber: "", - Manufacturer: "JetKVM", + Manufacturer: "KVM", Product: "USB Emulation Device", strictMode: true, } @@ -25,7 +25,7 @@ var ( Keyboard: true, MassStorage: true, } - usbGadgetName = "jetkvm" + usbGadgetName = "kvm" usbGadget *UsbGadget ) @@ -109,7 +109,7 @@ func TestUsbGadgetUDCNotBoundAfterReportDescrChanged(t *testing.T) { udc := udcs[0] assert.NotNil(udc, "UDC should exist") - udcStr, err := os.ReadFile("/sys/kernel/config/usb_gadget/jetkvm/UDC") + udcStr, err := os.ReadFile("/sys/kernel/config/usb_gadget/kvm/UDC") assert.Nil(err, "usb_gadget/UDC should exist") assert.Equal(strings.TrimSpace(udc), strings.TrimSpace(string(udcStr)), "UDC should be the same") } diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go index 1c4f9c3..ea0059b 100644 --- a/internal/usbgadget/config.go +++ b/internal/usbgadget/config.go @@ -34,7 +34,8 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{ "bcdDevice": "0100", }, configAttrs: gadgetAttributes{ - "MaxPower": "250", // in unit of 2mA + "MaxPower": "250", // in unit of 2mA + "bmAttributes": "0xa0", // 0x80 = bus-powered, 0xa0 = bus-powered + remote wakeup }, }, "base_info": { @@ -43,8 +44,8 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{ configPath: []string{"strings", "0x409"}, attrs: gadgetAttributes{ "serialnumber": "", - "manufacturer": "JetKVM", - "product": "JetKVM USB Emulation Device", + "manufacturer": "KVM", + "product": "KVM USB Emulation Device", }, configAttrs: gadgetAttributes{ "configuration": "Config 1: HID", @@ -59,6 +60,23 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{ // mass storage "mass_storage_base": massStorageBaseConfig, "mass_storage_lun0": massStorageLun0Config, + // audio + "audio": { + order: 4000, + device: "uac1.usb0", + path: []string{"functions", "uac1.usb0"}, + configPath: []string{"uac1.usb0"}, + attrs: gadgetAttributes{ + "p_chmask": "3", + "p_srate": "48000", + "p_ssize": "2", + "p_volume_present": "0", + "c_chmask": "3", + "c_srate": "48000", + "c_ssize": "2", + "c_volume_present": "0", + }, + }, } func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool { @@ -73,6 +91,8 @@ func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool { return u.enabledDevices.MassStorage case "mass_storage_lun0": return u.enabledDevices.MassStorage + case "audio": + return u.enabledDevices.Audio default: return true } diff --git a/internal/usbgadget/mass_storage.go b/internal/usbgadget/mass_storage.go index 41c1521..70649a7 100644 --- a/internal/usbgadget/mass_storage.go +++ b/internal/usbgadget/mass_storage.go @@ -18,9 +18,8 @@ var massStorageLun0Config = gadgetConfigItem{ "ro": "1", "removable": "1", "file": "\n", - // the additional whitespace is intentional to avoid the "JetKVM V irtual Media" string - // https://github.com/jetkvm/rv1106-system/blob/778133a1c153041e73f7de86c9c434a2753ea65d/sysdrv/source/uboot/u-boot/drivers/usb/gadget/f_mass_storage.c#L2556 + // the additional whitespace is intentional to avoid the "KVM V irtual Media" string // Vendor (8 chars), product (16 chars) - "inquiry_string": "JetKVM Virtual Media", + "inquiry_string": "KVM Virtual Media", }, } diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index fb28297..d6d0133 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -9,7 +9,8 @@ import ( "sync" "time" - "github.com/jetkvm/kvm/internal/logging" + "kvm/internal/logging" + "github.com/rs/zerolog" ) @@ -19,6 +20,7 @@ type Devices struct { RelativeMouse bool `json:"relative_mouse"` Keyboard bool `json:"keyboard"` MassStorage bool `json:"mass_storage"` + Audio bool `json:"audio"` } // Config is a struct that represents the customizations for a USB gadget. diff --git a/internal/websecure/ed25519_test.go b/internal/websecure/ed25519_test.go deleted file mode 100644 index 0753be0..0000000 --- a/internal/websecure/ed25519_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package websecure - -import ( - "os" - "testing" -) - -var ( - fixtureEd25519Certificate = `-----BEGIN CERTIFICATE----- -MIIBQDCB86ADAgECAhQdB4qB6dV0/u1lwhJofQgkmjjV1zAFBgMrZXAwLzELMAkG -A1UEBhMCREUxIDAeBgNVBAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMB4XDTI1 -MDUyMzEyNTkyN1oXDTI3MDQyMzEyNTkyN1owLzELMAkGA1UEBhMCREUxIDAeBgNV -BAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMCowBQYDK2VwAyEA9tLyoulJn7Ev -bf8kuD1ZGdA092773pCRjFEDKpXHonyjITAfMB0GA1UdDgQWBBRkmrVMfsLY57iy -r/0POP0S4QxCADAFBgMrZXADQQBfTRvqavLHDYQiKQTgbGod+Yn+fIq2lE584+1U -C4wh9peIJDFocLBEAYTQpEMKxa4s0AIRxD+a7aCS5oz0e/0I ------END CERTIFICATE-----` - - fixtureEd25519PrivateKey = `-----BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEIKV08xUsLRHBfMXqZwxVRzIbViOp8G7aQGjPvoRFjujB ------END PRIVATE KEY-----` - - certStore *CertStore - certSigner *SelfSigner -) - -func TestMain(m *testing.M) { - tlsStorePath, err := os.MkdirTemp("", "jktls.*") - if err != nil { - defaultLogger.Fatal().Err(err).Msg("failed to create temp directory") - } - - certStore = NewCertStore(tlsStorePath, nil) - certStore.LoadCertificates() - - certSigner = NewSelfSigner( - certStore, - nil, - "ci.jetkvm.com", - "JetKVM", - "JetKVM", - "JetKVM", - ) - - m.Run() - - os.RemoveAll(tlsStorePath) -} - -func TestSaveEd25519Certificate(t *testing.T) { - err, _ := certStore.ValidateAndSaveCertificate("ed25519-test.jetkvm.com", fixtureEd25519Certificate, fixtureEd25519PrivateKey, true) - if err != nil { - t.Fatalf("failed to save certificate: %v", err) - } -} diff --git a/io.go b/io.go new file mode 100644 index 0000000..1f7c688 --- /dev/null +++ b/io.go @@ -0,0 +1,118 @@ +package kvm + +import ( + "fmt" + "os" + "strconv" +) + +const ( + gpioBasePath = "/sys/class/gpio" + ledGreenPath = "/sys/class/leds/led:g" + ledYellowPath = "/sys/class/leds/led:y" +) + +func exportGPIO(pin int) error { + exportFile := gpioBasePath + "/export" + return os.WriteFile(exportFile, []byte(strconv.Itoa(pin)), 0644) +} + +func unexportGPIO(pin int) error { + unexportFile := gpioBasePath + "/unexport" + return os.WriteFile(unexportFile, []byte(strconv.Itoa(pin)), 0644) +} + +func isGPIOExported(pin int) bool { + gpioPath := fmt.Sprintf("%s/gpio%d", gpioBasePath, pin) + _, err := os.Stat(gpioPath) + return err == nil +} + +func setGPIODirection(pin int, direction string) error { + if !isGPIOExported(pin) { + if err := exportGPIO(pin); err != nil { + return fmt.Errorf("failed to export GPIO: %v", err) + } + } + directionFile := fmt.Sprintf("%s/gpio%d/direction", gpioBasePath, pin) + return os.WriteFile(directionFile, []byte(direction), 0644) +} + +func setGPIOValue(pin int, status bool) error { + var value int + if status { + value = 1 + } else { + value = 0 + } + if !isGPIOExported(pin) { + if err := exportGPIO(pin); err != nil { + return fmt.Errorf("failed to export GPIO: %v", err) + } + if err := setGPIODirection(pin, "out"); err != nil { + return fmt.Errorf("failed to set GPIO direction: %v", err) + } + } + valueFile := fmt.Sprintf("%s/gpio%d/value", gpioBasePath, pin) + return os.WriteFile(valueFile, []byte(strconv.Itoa(value)), 0644) +} + +func setLedMode(ledConfigPath string, mode string) error { + if mode == "network-link" { + err := os.WriteFile(ledConfigPath+"/trigger", []byte("netdev"), 0644) + if err != nil { + return fmt.Errorf("failed to set LED trigger: %v", err) + } + err = os.WriteFile(ledConfigPath+"/device_name", []byte("eth0"), 0644) + if err != nil { + return fmt.Errorf("failed to set LED device name: %v", err) + } + err = os.WriteFile(ledConfigPath+"/link", []byte("1"), 0644) + if err != nil { + return fmt.Errorf("failed to set LED link: %v", err) + } + } else if mode == "network-tx" { + err := os.WriteFile(ledConfigPath+"/trigger", []byte("netdev"), 0644) + if err != nil { + return fmt.Errorf("failed to set LED trigger: %v", err) + } + err = os.WriteFile(ledConfigPath+"/device_name", []byte("eth0"), 0644) + if err != nil { + return fmt.Errorf("failed to set LED device name: %v", err) + } + err = os.WriteFile(ledConfigPath+"/tx", []byte("1"), 0644) + if err != nil { + return fmt.Errorf("failed to set LED tx: %v", err) + } + } else if mode == "network-rx" { + err := os.WriteFile(ledConfigPath+"/trigger", []byte("netdev"), 0644) + if err != nil { + return fmt.Errorf("failed to set LED trigger: %v", err) + } + err = os.WriteFile(ledConfigPath+"/device_name", []byte("eth0"), 0644) + if err != nil { + return fmt.Errorf("failed to set LED device name: %v", err) + } + err = os.WriteFile(ledConfigPath+"/rx", []byte("1"), 0644) + if err != nil { + return fmt.Errorf("failed to set LED rx: %v", err) + } + } else if mode == "kernel-activity" { + err := os.WriteFile(ledConfigPath+"/trigger", []byte("activity"), 0644) + if err != nil { + return fmt.Errorf("failed to set LED trigger: %v", err) + } + } else { + return fmt.Errorf("invalid LED mode: %s", mode) + } + return nil +} + +func initGPIO() { + LoadConfig() + // IO0: GPIO58 IO1: GPIO59 + _ = setGPIOValue(58, config.IO0Status) + _ = setGPIOValue(59, config.IO1Status) + _ = setLedMode(ledYellowPath, config.LEDYellowMode) + _ = setLedMode(ledGreenPath, config.LEDGreenMode) +} diff --git a/jsonrpc.go b/jsonrpc.go index 258828a..fbd2ede 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -7,7 +7,6 @@ import ( "fmt" "os" "os/exec" - "path/filepath" "reflect" "strconv" "time" @@ -15,7 +14,7 @@ import ( "github.com/pion/webrtc/v4" "go.bug.st/serial" - "github.com/jetkvm/kvm/internal/usbgadget" + "kvm/internal/usbgadget" ) type JSONRPCRequest struct { @@ -263,6 +262,17 @@ func rpcSetDevChannelState(enabled bool) error { return nil } +func rpcGetLocalUpdateStatus() (*LocalMetadata, error) { + var localStatus LocalMetadata + systemVersionLocal, appVersionLocal, err := GetLocalVersion() + if err != nil { + return nil, fmt.Errorf("failed to get local version: %w", err) + } + localStatus.AppVersion = appVersionLocal.String() + localStatus.SystemVersion = systemVersionLocal.String() + return &localStatus, nil +} + func rpcGetUpdateStatus() (*UpdateStatus, error) { includePreRelease := config.IncludePreRelease updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease) @@ -353,10 +363,28 @@ func rpcGetBacklightSettings() (*BacklightSettings, error) { }, nil } +func rpcSetTimeZone(timeZone string) error { + var err error + _, err = CallDisplayCtrlAction("set_timezone", map[string]interface{}{"timezone": timeZone}) + + if err == nil { + config.TimeZone = timeZone + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + } + + return err +} + +func rpcGetTimeZone() (string, error) { + return config.TimeZone, nil +} + const ( - devModeFile = "/userdata/jetkvm/devmode.enable" - sshKeyDir = "/userdata/dropbear/.ssh" - sshKeyFile = "/userdata/dropbear/.ssh/authorized_keys" + devModeFile = "/userdata/picokvm/devmode.enable" + sshKeyDir = "/userdata/openssh/.ssh" + sshKeyFile = "/userdata/openssh/.ssh/authorized_keys" ) type DevModeState struct { @@ -382,42 +410,6 @@ func rpcGetDevModeState() (DevModeState, error) { }, nil } -func rpcSetDevModeState(enabled bool) error { - if enabled { - if _, err := os.Stat(devModeFile); os.IsNotExist(err) { - if err := os.MkdirAll(filepath.Dir(devModeFile), 0755); err != nil { - return fmt.Errorf("failed to create directory for devmode file: %w", err) - } - if err := os.WriteFile(devModeFile, []byte{}, 0644); err != nil { - return fmt.Errorf("failed to create devmode file: %w", err) - } - } else { - logger.Debug().Msg("dev mode already enabled") - return nil - } - } else { - if _, err := os.Stat(devModeFile); err == nil { - if err := os.Remove(devModeFile); err != nil { - return fmt.Errorf("failed to remove devmode file: %w", err) - } - } else if os.IsNotExist(err) { - logger.Debug().Msg("dev mode already disabled") - return nil - } else { - return fmt.Errorf("error checking dev mode file: %w", err) - } - } - - cmd := exec.Command("dropbear.sh") - output, err := cmd.CombinedOutput() - if err != nil { - logger.Warn().Err(err).Bytes("output", output).Msg("Failed to start/stop SSH") - return fmt.Errorf("failed to start/stop SSH, you may need to reboot for changes to take effect") - } - - return nil -} - func rpcGetSSHKeyState() (string, error) { keyData, err := os.ReadFile(sshKeyFile) if err != nil { @@ -680,26 +672,6 @@ func rpcResetConfig() error { return nil } -type DCPowerState struct { - IsOn bool `json:"isOn"` - Voltage float64 `json:"voltage"` - Current float64 `json:"current"` - Power float64 `json:"power"` -} - -func rpcGetDCPowerState() (DCPowerState, error) { - return dcState, nil -} - -func rpcSetDCPowerState(enabled bool) error { - logger.Info().Bool("enabled", enabled).Msg("Setting DC power state") - err := setDCPowerState(enabled) - if err != nil { - return fmt.Errorf("failed to set DC power state: %w", err) - } - return nil -} - func rpcGetActiveExtension() (string, error) { return config.ActiveExtension, nil } @@ -708,55 +680,13 @@ func rpcSetActiveExtension(extensionId string) error { if config.ActiveExtension == extensionId { return nil } - switch config.ActiveExtension { - case "atx-power": - _ = unmountATXControl() - case "dc-power": - _ = unmountDCControl() - } config.ActiveExtension = extensionId if err := SaveConfig(); err != nil { return fmt.Errorf("failed to save config: %w", err) } - switch extensionId { - case "atx-power": - _ = mountATXControl() - case "dc-power": - _ = mountDCControl() - } return nil } -func rpcSetATXPowerAction(action string) error { - logger.Debug().Str("action", action).Msg("Executing ATX power action") - switch action { - case "power-short": - logger.Debug().Msg("Simulating short power button press") - return pressATXPowerButton(200 * time.Millisecond) - case "power-long": - logger.Debug().Msg("Simulating long power button press") - return pressATXPowerButton(5 * time.Second) - case "reset": - logger.Debug().Msg("Simulating reset button press") - return pressATXResetButton(200 * time.Millisecond) - default: - return fmt.Errorf("invalid action: %s", action) - } -} - -type ATXState struct { - Power bool `json:"power"` - HDD bool `json:"hdd"` -} - -func rpcGetATXState() (ATXState, error) { - state := ATXState{ - Power: ledPWRState, - HDD: ledHDDState, - } - return state, nil -} - type SerialSettings struct { BaudRate string `json:"baudRate"` DataBits string `json:"dataBits"` @@ -885,22 +815,6 @@ func rpcSetUsbDeviceState(device string, enabled bool) error { return updateUsbRelatedConfig() } -func rpcSetCloudUrl(apiUrl string, appUrl string) error { - currentCloudURL := config.CloudURL - config.CloudURL = apiUrl - config.CloudAppURL = appUrl - - if currentCloudURL != apiUrl { - disconnectCloud(fmt.Errorf("cloud url changed from %s to %s", currentCloudURL, apiUrl)) - } - - if err := SaveConfig(); err != nil { - return fmt.Errorf("failed to save config: %w", err) - } - - return nil -} - func rpcGetKeyboardLayout() (string, error) { return config.KeyboardLayout, nil } @@ -1025,83 +939,178 @@ func rpcSetLocalLoopbackOnly(enabled bool) error { return nil } -var rpcHandlers = map[string]RPCHandler{ - "ping": {Func: rpcPing}, - "reboot": {Func: rpcReboot, Params: []string{"force"}}, - "getDeviceID": {Func: rpcGetDeviceID}, - "deregisterDevice": {Func: rpcDeregisterDevice}, - "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"}}, - "getKeyboardLedState": {Func: rpcGetKeyboardLedState}, - "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, - "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, - "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, - "getVideoState": {Func: rpcGetVideoState}, - "getUSBState": {Func: rpcGetUSBState}, - "unmountImage": {Func: rpcUnmountImage}, - "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, - "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, - "getJigglerState": {Func: rpcGetJigglerState}, - "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, - "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, - "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, - "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, - "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, - "getEDID": {Func: rpcGetEDID}, - "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, - "getDevChannelState": {Func: rpcGetDevChannelState}, - "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, - "getUpdateStatus": {Func: rpcGetUpdateStatus}, - "tryUpdate": {Func: rpcTryUpdate}, - "getDevModeState": {Func: rpcGetDevModeState}, - "setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}}, - "getSSHKeyState": {Func: rpcGetSSHKeyState}, - "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, - "getTLSState": {Func: rpcGetTLSState}, - "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, - "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, - "getMassStorageMode": {Func: rpcGetMassStorageMode}, - "isUpdatePending": {Func: rpcIsUpdatePending}, - "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, - "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, - "getUsbConfig": {Func: rpcGetUsbConfig}, - "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, - "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, - "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, - "getStorageSpace": {Func: rpcGetStorageSpace}, - "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, - "mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, - "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, - "listStorageFiles": {Func: rpcListStorageFiles}, - "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, - "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, - "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, - "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, - "resetConfig": {Func: rpcResetConfig}, - "setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}}, - "getDisplayRotation": {Func: rpcGetDisplayRotation}, - "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, - "getBacklightSettings": {Func: rpcGetBacklightSettings}, - "getDCPowerState": {Func: rpcGetDCPowerState}, - "setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}}, - "getActiveExtension": {Func: rpcGetActiveExtension}, - "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, - "getATXState": {Func: rpcGetATXState}, - "setATXPowerAction": {Func: rpcSetATXPowerAction, Params: []string{"action"}}, - "getSerialSettings": {Func: rpcGetSerialSettings}, - "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, - "getUsbDevices": {Func: rpcGetUsbDevices}, - "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, - "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, - "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, - "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, - "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}}, - "getKeyboardMacros": {Func: getKeyboardMacros}, - "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, - "getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly}, - "setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, +type IOSettings struct { + IO0Status bool `json:"io0Status"` + IO1Status bool `json:"io1Status"` +} + +func rpcGetIOSettings() (IOSettings, error) { + LoadConfig() + settings := IOSettings{ + IO0Status: config.IO0Status, + IO1Status: config.IO1Status, + } + + return settings, nil +} + +func rpcSetIOSettings(settings IOSettings) error { + LoadConfig() + // IO0: GPIO58 IO1: GPIO59 + _ = setGPIOValue(58, settings.IO0Status) + _ = setGPIOValue(59, settings.IO1Status) + + config.IO0Status = settings.IO0Status + config.IO1Status = settings.IO1Status + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + return nil +} + +func rpcGetAudioMode() (string, error) { + return config.AudioMode, nil +} + +func rpcSetAudioMode(mode string) error { + config.AudioMode = mode + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + return nil +} + +func rpcSetLedGreenMode(mode string) error { + err := setLedMode(ledGreenPath, mode) + if err != nil { + return err + } + + config.LEDGreenMode = mode + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + return nil +} + +func rpcSetLedYellowMode(mode string) error { + err := setLedMode(ledYellowPath, mode) + if err != nil { + return err + } + + config.LEDYellowMode = mode + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + return nil +} + +func rpcGetLedGreenMode() (string, error) { + return config.LEDGreenMode, nil +} + +func rpcGetLedYellowMode() (string, error) { + return config.LEDYellowMode, nil +} + +var rpcHandlers = map[string]RPCHandler{ + "ping": {Func: rpcPing}, + "reboot": {Func: rpcReboot, Params: []string{"force"}}, + "getDeviceID": {Func: rpcGetDeviceID}, + "getNetworkState": {Func: rpcGetNetworkState}, + "getNetworkSettings": {Func: rpcGetNetworkSettings}, + "setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}}, + "renewDHCPLease": {Func: rpcRenewDHCPLease}, + "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, + "getKeyboardLedState": {Func: rpcGetKeyboardLedState}, + "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, + "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}}, + "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, + "getVideoState": {Func: rpcGetVideoState}, + "getUSBState": {Func: rpcGetUSBState}, + "unmountImage": {Func: rpcUnmountImage}, + "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, + "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, + "getJigglerState": {Func: rpcGetJigglerState}, + "sendUsbWakeupSignal": {Func: rpcSendUsbWakeupSignal}, + "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, + "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, + "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, + "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, + "setAutoUpdateState": {Func: rpcSetAutoUpdateState, Params: []string{"enabled"}}, + "getEDID": {Func: rpcGetEDID}, + "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, + "getDevChannelState": {Func: rpcGetDevChannelState}, + "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, + "getLocalUpdateStatus": {Func: rpcGetLocalUpdateStatus}, + "getUpdateStatus": {Func: rpcGetUpdateStatus}, + "tryUpdate": {Func: rpcTryUpdate}, + "getDevModeState": {Func: rpcGetDevModeState}, + "getSSHKeyState": {Func: rpcGetSSHKeyState}, + "setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}}, + "getTLSState": {Func: rpcGetTLSState}, + "setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}}, + "setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}}, + "getMassStorageMode": {Func: rpcGetMassStorageMode}, + "isUpdatePending": {Func: rpcIsUpdatePending}, + "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, + "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, + "getUsbConfig": {Func: rpcGetUsbConfig}, + "setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}}, + "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, + "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, + "getStorageSpace": {Func: rpcGetStorageSpace}, + "getSDStorageSpace": {Func: rpcGetSDStorageSpace}, + "resetSDStorage": {Func: rpcResetSDStorage}, + "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, + "mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, + "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, + "mountWithSDStorage": {Func: rpcMountWithSDStorage, Params: []string{"filename", "mode"}}, + "listStorageFiles": {Func: rpcListStorageFiles}, + "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, + "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, + "listSDStorageFiles": {Func: rpcListSDStorageFiles}, + "deleteSDStorageFile": {Func: rpcDeleteSDStorageFile, Params: []string{"filename"}}, + "startSDStorageFileUpload": {Func: rpcStartSDStorageFileUpload, Params: []string{"filename", "size"}}, + "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, + "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, + "resetConfig": {Func: rpcResetConfig}, + "setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}}, + "getDisplayRotation": {Func: rpcGetDisplayRotation}, + "setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}}, + "getBacklightSettings": {Func: rpcGetBacklightSettings}, + "setTimeZone": {Func: rpcSetTimeZone, Params: []string{"timeZone"}}, + "getTimeZone": {Func: rpcGetTimeZone}, + "setLedGreenMode": {Func: rpcSetLedGreenMode, Params: []string{"mode"}}, + "setLedYellowMode": {Func: rpcSetLedYellowMode, Params: []string{"mode"}}, + "getLedGreenMode": {Func: rpcGetLedGreenMode}, + "getLedYellowMode": {Func: rpcGetLedYellowMode}, + "getActiveExtension": {Func: rpcGetActiveExtension}, + "setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}}, + "getSerialSettings": {Func: rpcGetSerialSettings}, + "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, + "getUsbDevices": {Func: rpcGetUsbDevices}, + "setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}}, + "setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}}, + "getKeyboardLayout": {Func: rpcGetKeyboardLayout}, + "setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}}, + "getKeyboardMacros": {Func: getKeyboardMacros}, + "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, + "getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly}, + "setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, + "getIOSettings": {Func: rpcGetIOSettings}, + "setIOSettings": {Func: rpcSetIOSettings, Params: []string{"settings"}}, + "getSDMountStatus": {Func: rpcGetSDMountStatus}, + "loginTailScale": {Func: rpcLoginTailScale, Params: []string{"xEdge"}}, + "logoutTailScale": {Func: rpcLogoutTailScale}, + "canelTailScale": {Func: rpcCanelTailScale}, + "getTailScaleSettings": {Func: rpcGetTailScaleSettings}, + "loginZeroTier": {Func: rpcLoginZeroTier, Params: []string{"networkID"}}, + "logoutZeroTier": {Func: rpcLogoutZeroTier, Params: []string{"networkID"}}, + "getZeroTierSettings": {Func: rpcGetZeroTierSettings}, + "setUpdateSource": {Func: rpcSetUpdateSource, Params: []string{"source"}}, + "getAudioMode": {Func: rpcGetAudioMode}, + "setAudioMode": {Func: rpcSetAudioMode, Params: []string{"mode"}}, } diff --git a/log.go b/log.go index b353a2c..4ef7a37 100644 --- a/log.go +++ b/log.go @@ -1,7 +1,8 @@ package kvm import ( - "github.com/jetkvm/kvm/internal/logging" + "kvm/internal/logging" + "github.com/rs/zerolog" ) @@ -10,12 +11,14 @@ func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) e } var ( - logger = logging.GetSubsystemLogger("jetkvm") + logger = logging.GetSubsystemLogger("kvm") networkLogger = logging.GetSubsystemLogger("network") + vpnLogger = logging.GetSubsystemLogger("vpn") cloudLogger = logging.GetSubsystemLogger("cloud") websocketLogger = logging.GetSubsystemLogger("websocket") webrtcLogger = logging.GetSubsystemLogger("webrtc") - nativeLogger = logging.GetSubsystemLogger("native") + videoLogger = logging.GetSubsystemLogger("video") + audioLogger = logging.GetSubsystemLogger("audio") nbdLogger = logging.GetSubsystemLogger("nbd") timesyncLogger = logging.GetSubsystemLogger("timesync") jsonRpcLogger = logging.GetSubsystemLogger("jsonrpc") diff --git a/main.go b/main.go index c25d8b8..1762c6d 100644 --- a/main.go +++ b/main.go @@ -28,10 +28,10 @@ func Main() { logger.Info(). Interface("system_version", systemVersionLocal). Interface("app_version", appVersionLocal). - Msg("starting JetKVM") + Msg("starting KVM") go runWatchdog() - go confirmCurrentSystem() + go confirmCurrentSystem() //A/B system http.DefaultClient.Timeout = 1 * time.Minute @@ -58,25 +58,62 @@ func Main() { logger.Error().Err(err).Msg("failed to initialize mDNS") os.Exit(1) } + //if mDNS != nil { + // _ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode()) + // _ = mDNS.SetLocalNames([]string{ + // networkState.GetHostname(), + // networkState.GetFQDN(), + // }, true) + //} // Initialize native ctrl socket server - StartNativeCtrlSocketServer() + StartVideoCtrlSocketServer() // Initialize native video socket server - StartNativeVideoSocketServer() + StartVideoDataSocketServer() + + // Initialize native audio socket server + StartAudioCtrlSocketServer() + + StartVpnCtrlSocketServer() + + StartDisplayCtrlSocketServer() initPrometheus() go func() { - err = ExtractAndRunNativeBin() + err = ExtractAndRunVideoBin() if err != nil { - logger.Warn().Err(err).Msg("failed to extract and run native bin") + logger.Warn().Err(err).Msg("failed to extract and run video bin") //TODO: prepare an error message screen buffer to show on kvm screen } + + err = ExtractAndRunDisplayBin() + if err != nil { + logger.Warn().Err(err).Msg("failed to extract and run display bin") + //TODO: prepare an error message screen buffer to show on kvm screen + } + + err = ExtractAndRunAudioBin() + if err != nil { + logger.Warn().Err(err).Msg("failed to extract and run audio bin") + //TODO: prepare an error message screen buffer to show on kvm screen + } + + err = ExtractAndRunVpnBin() + if err != nil { + logger.Warn().Err(err).Msg("failed to extract and run vpn bin") + //TODO: prepare an error message screen buffer to show on kvm screen + } + }() // initialize usb gadget initUsbGadget() + + // initialize GPIO + initGPIO() + if err := setInitialVirtualMediaState(); err != nil { logger.Warn().Err(err).Msg("failed to set initial virtual media state") } @@ -89,26 +126,30 @@ func Main() { // initialize display initDisplay() - go func() { - time.Sleep(15 * time.Minute) - for { - logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING") - if !config.AutoUpdateEnabled { - return - } - if currentSession != nil { - logger.Debug().Msg("skipping update since a session is active") - time.Sleep(1 * time.Minute) - continue - } - includePreRelease := config.IncludePreRelease - err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease) - if err != nil { - logger.Warn().Err(err).Msg("failed to auto update") - } - time.Sleep(1 * time.Hour) - } - }() + // Initialize VPN + initVPN() + + //Auto update + //go func() { + // time.Sleep(15 * time.Minute) + // for { + // logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING") + // if !config.AutoUpdateEnabled { + // return + // } + // if currentSession != nil { + // logger.Debug().Msg("skipping update since a session is active") + // time.Sleep(1 * time.Minute) + // continue + // } + // includePreRelease := config.IncludePreRelease + // err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease) + // if err != nil { + // logger.Warn().Err(err).Msg("failed to auto update") + // } + // time.Sleep(1 * time.Hour) + // } + //}() //go RunFuseServer() go RunWebServer() @@ -118,14 +159,11 @@ func Main() { startWebSecureServer() } - // As websocket client already checks if the cloud token is set, we can start it here. - go RunWebsocketClient() - initSerialPort() sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs - logger.Info().Msg("JetKVM Shutting Down") + logger.Info().Msg("KVM Shutting Down") //if fuseServer != nil { // err := setMassStorageImage(" ") // if err != nil { diff --git a/mdns.go b/mdns.go index d7a3b55..c115f18 100644 --- a/mdns.go +++ b/mdns.go @@ -1,7 +1,7 @@ package kvm import ( - "github.com/jetkvm/kvm/internal/mdns" + "kvm/internal/mdns" ) var mDNS *mdns.MDNS diff --git a/native.go b/native.go index e2da36a..f87af3a 100644 --- a/native.go +++ b/native.go @@ -1,19 +1,15 @@ package kvm import ( - "bytes" "encoding/json" "errors" "fmt" - "io" "net" "os" "os/exec" "sync" "time" - "github.com/jetkvm/kvm/resource" - "github.com/pion/webrtc/v4/pkg/media" ) @@ -43,8 +39,8 @@ var ongoingRequests = make(map[int32]chan *CtrlResponse) var lock = &sync.Mutex{} var ( - nativeCmd *exec.Cmd - nativeCmdLock = &sync.Mutex{} + videoCmd *exec.Cmd + videoCmdLock = &sync.Mutex{} ) func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) { @@ -66,7 +62,7 @@ func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse return nil, fmt.Errorf("error marshaling ctrl action: %w", err) } - scopedLogger := nativeLogger.With(). + scopedLogger := videoLogger.With(). Str("action", ctrlAction.Action). Interface("params", ctrlAction.Params).Logger() @@ -104,8 +100,8 @@ func WriteCtrlMessage(message []byte) error { return err } -var nativeCtrlSocketListener net.Listener //nolint:unused -var nativeVideoSocketListener net.Listener //nolint:unused +var videoCtrlSocketListener net.Listener //nolint:unused +var videoSocketListener net.Listener //nolint:unused var ctrlClientConnected = make(chan struct{}) @@ -113,8 +109,8 @@ func waitCtrlClientConnected() { <-ctrlClientConnected } -func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener { - scopedLogger := nativeLogger.With(). +func StartVideoSocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener { + scopedLogger := videoLogger.With(). Str("socket_path", socketPath). Logger() @@ -160,20 +156,20 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC return listener } -func StartNativeCtrlSocketServer() { - nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true) - nativeLogger.Debug().Msg("native app ctrl sock started") +func StartVideoCtrlSocketServer() { + videoCtrlSocketListener = StartVideoSocketServer("/var/run/kvm_ctrl.sock", handleCtrlClient, true) + videoLogger.Debug().Msg("native app ctrl sock started") } -func StartNativeVideoSocketServer() { - nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false) - nativeLogger.Debug().Msg("native app video sock started") +func StartVideoDataSocketServer() { + videoSocketListener = StartVideoSocketServer("/var/run/kvm_video.sock", handleVideoClient, false) + videoLogger.Debug().Msg("native app video sock started") } func handleCtrlClient(conn net.Conn) { defer conn.Close() - scopedLogger := nativeLogger.With(). + scopedLogger := videoLogger.With(). Str("addr", conn.RemoteAddr().String()). Str("type", "ctrl"). Logger() @@ -224,7 +220,7 @@ func handleCtrlClient(conn net.Conn) { func handleVideoClient(conn net.Conn) { defer conn.Close() - scopedLogger := nativeLogger.With(). + scopedLogger := videoLogger.With(). Str("addr", conn.RemoteAddr().String()). Str("type", "video"). Logger() @@ -251,63 +247,60 @@ func handleVideoClient(conn net.Conn) { } } -func startNativeBinaryWithLock(binaryPath string) (*exec.Cmd, error) { - nativeCmdLock.Lock() - defer nativeCmdLock.Unlock() +func startVideoBinaryWithLock(binaryPath string) (*exec.Cmd, error) { + videoCmdLock.Lock() + defer videoCmdLock.Unlock() - cmd, err := startNativeBinary(binaryPath) + cmd, err := startVideoBinary(binaryPath) if err != nil { return nil, err } - nativeCmd = cmd + videoCmd = cmd return cmd, nil } -func restartNativeBinary(binaryPath string) error { +func restartVideoBinary(binaryPath string) error { time.Sleep(10 * time.Second) // restart the binary - nativeLogger.Info().Msg("restarting jetkvm_native binary") - cmd, err := startNativeBinary(binaryPath) + videoLogger.Info().Msg("restarting kvm_video binary") + cmd, err := startVideoBinary(binaryPath) if err != nil { - nativeLogger.Warn().Err(err).Msg("failed to restart binary") + videoLogger.Warn().Err(err).Msg("failed to restart binary") } - nativeCmd = cmd + videoCmd = cmd return err } -func superviseNativeBinary(binaryPath string) error { - nativeCmdLock.Lock() - defer nativeCmdLock.Unlock() +func superviseVideoBinary(binaryPath string) error { + videoCmdLock.Lock() + defer videoCmdLock.Unlock() - if nativeCmd == nil || nativeCmd.Process == nil { - return restartNativeBinary(binaryPath) + if videoCmd == nil || videoCmd.Process == nil { + return restartVideoBinary(binaryPath) } - err := nativeCmd.Wait() + err := videoCmd.Wait() if err == nil { - nativeLogger.Info().Err(err).Msg("jetkvm_native binary exited with no error") + videoLogger.Info().Err(err).Msg("kvm_video binary exited with no error") } else if exiterr, ok := err.(*exec.ExitError); ok { - nativeLogger.Warn().Int("exit_code", exiterr.ExitCode()).Msg("jetkvm_native binary exited with error") + videoLogger.Warn().Int("exit_code", exiterr.ExitCode()).Msg("kvm_video binary exited with error") } else { - nativeLogger.Warn().Err(err).Msg("jetkvm_native binary exited with unknown error") + videoLogger.Warn().Err(err).Msg("kvm_video binary exited with unknown error") } - return restartNativeBinary(binaryPath) + return restartVideoBinary(binaryPath) } -func ExtractAndRunNativeBin() error { - binaryPath := "/userdata/jetkvm/bin/jetkvm_native" - if err := ensureBinaryUpdated(binaryPath); err != nil { - return fmt.Errorf("failed to extract binary: %w", err) - } +func ExtractAndRunVideoBin() error { + binaryPath := "/userdata/picokvm/bin/kvm_video" // Make the binary executable if err := os.Chmod(binaryPath, 0755); err != nil { return fmt.Errorf("failed to make binary executable: %w", err) } // Run the binary in the background - cmd, err := startNativeBinaryWithLock(binaryPath) + cmd, err := startVideoBinaryWithLock(binaryPath) if err != nil { return fmt.Errorf("failed to start binary: %w", err) } @@ -317,12 +310,12 @@ func ExtractAndRunNativeBin() error { for { select { case <-appCtx.Done(): - nativeLogger.Info().Msg("stopping native binary supervisor") + videoLogger.Info().Msg("stopping native binary supervisor") return default: - err := superviseNativeBinary(binaryPath) + err := superviseVideoBinary(binaryPath) if err != nil { - nativeLogger.Warn().Err(err).Msg("failed to supervise native binary") + videoLogger.Warn().Err(err).Msg("failed to supervise native binary") time.Sleep(1 * time.Second) // Add a short delay to prevent rapid successive calls } } @@ -331,83 +324,26 @@ func ExtractAndRunNativeBin() error { go func() { <-appCtx.Done() - nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process") + videoLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process") err := cmd.Process.Kill() if err != nil { - nativeLogger.Warn().Err(err).Msg("failed to kill process") + videoLogger.Warn().Err(err).Msg("failed to kill process") return } }() - nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("jetkvm_native binary started") - - return nil -} - -func shouldOverwrite(destPath string, srcHash []byte) bool { - if srcHash == nil { - nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, doing overwriting") - return true - } - - dstHash, err := os.ReadFile(destPath + ".sha256") - if err != nil { - nativeLogger.Debug().Msg("error reading existing jetkvm_native.sha256, doing overwriting") - return true - } - - return !bytes.Equal(srcHash, dstHash) -} - -func ensureBinaryUpdated(destPath string) error { - srcFile, err := resource.ResourceFS.Open("jetkvm_native") - if err != nil { - return err - } - defer srcFile.Close() - - srcHash, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256") - if err != nil { - nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, proceeding with update") - srcHash = nil - } - - _, err = os.Stat(destPath) - if shouldOverwrite(destPath, srcHash) || err != nil { - nativeLogger.Info(). - Interface("hash", srcHash). - Msg("writing jetkvm_native") - - _ = os.Remove(destPath) - destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755) - if err != nil { - return err - } - _, err = io.Copy(destFile, srcFile) - destFile.Close() - if err != nil { - return err - } - if srcHash != nil { - err = os.WriteFile(destPath+".sha256", srcHash, 0644) - if err != nil { - return err - } - } - nativeLogger.Info().Msg("jetkvm_native updated") - } + videoLogger.Info().Int("pid", cmd.Process.Pid).Msg("kvm_video binary started") return nil } // Restore the HDMI EDID value from the config. -// Called after successful connection to jetkvm_native. func restoreHdmiEdid() { if config.EdidString != "" { - nativeLogger.Info().Str("edid", config.EdidString).Msg("Restoring HDMI EDID") + videoLogger.Info().Str("edid", config.EdidString).Msg("Restoring HDMI EDID") _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString}) if err != nil { - nativeLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID") + videoLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID") } } } diff --git a/native_audio.go b/native_audio.go new file mode 100644 index 0000000..89f2e77 --- /dev/null +++ b/native_audio.go @@ -0,0 +1,279 @@ +package kvm + +import ( + "encoding/json" + "errors" + "fmt" + "net" + "os" + "os/exec" + "sync" + "time" +) + +var ( + audioCmd *exec.Cmd + audioCmdLock = &sync.Mutex{} +) + +var audioSocketConn net.Conn + +var audioOngoingRequests = make(map[int32]chan *CtrlResponse) + +var audioLock = &sync.Mutex{} + +func CallAudioCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) { + audioLock.Lock() + defer audioLock.Unlock() + ctrlAction := CtrlAction{ + Action: action, + Seq: seq, + Params: params, + } + + responseChan := make(chan *CtrlResponse) + audioOngoingRequests[seq] = responseChan + seq++ + + jsonData, err := json.Marshal(ctrlAction) + if err != nil { + delete(audioOngoingRequests, ctrlAction.Seq) + return nil, fmt.Errorf("error marshaling ctrl action: %w", err) + } + + scopedLogger := audioLogger.With(). + Str("action", ctrlAction.Action). + Interface("params", ctrlAction.Params).Logger() + + scopedLogger.Debug().Msg("sending audio ctrl action") + + err = WriteAudioCtrlMessage(jsonData) + if err != nil { + delete(audioOngoingRequests, ctrlAction.Seq) + return nil, ErrorfL(&scopedLogger, "error writing audio ctrl message", err) + } + + select { + case response := <-responseChan: + delete(audioOngoingRequests, seq) + if response.Error != "" { + return nil, ErrorfL( + &scopedLogger, + "error audio response: %s", + errors.New(response.Error), + ) + } + return response, nil + case <-time.After(5 * time.Second): + close(responseChan) + delete(audioOngoingRequests, seq) + return nil, ErrorfL(&scopedLogger, "timeout waiting for response", nil) + } +} + +func WriteAudioCtrlMessage(message []byte) error { + if audioSocketConn == nil { + return fmt.Errorf("audio socket not conn ected") + } + _, err := audioSocketConn.Write(message) + return err +} + +var audioCtrlSocketListener net.Listener + +var audioCtrlClientConnected = make(chan struct{}) + +func waitAudioCtrlClientConnected() { + <-audioCtrlClientConnected +} + +func StartAudioSocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener { + scopedLogger := audioLogger.With(). + Str("socket_path", socketPath). + Logger() + + // Remove the socket file if it already exists + if _, err := os.Stat(socketPath); err == nil { + if err := os.Remove(socketPath); err != nil { + scopedLogger.Warn().Err(err).Msg("failed to remove existing socket file") + os.Exit(1) + } + } + + listener, err := net.Listen("unixpacket", socketPath) + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to start server") + os.Exit(1) + } + + scopedLogger.Info().Msg("server listening") + + go func() { + for { + conn, err := listener.Accept() + + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to accept socket") + continue + } + if isCtrl { + // check if the channel is closed + select { + case <-audioCtrlClientConnected: + scopedLogger.Debug().Msg("audio ctrl client reconnected") + default: + close(audioCtrlClientConnected) + scopedLogger.Debug().Msg("first audio ctrl socket client connected") + } + } + + //conn.Write([]byte("[handleAudioCtrlClient]audio sock test")) + go handleClient(conn) + } + }() + + return listener +} + +func StartAudioCtrlSocketServer() { + audioCtrlSocketListener = StartAudioSocketServer("/var/run/kvm_audio.sock", handleAudioCtrlClient, true) + audioLogger.Debug().Msg("audio ctrl sock started") +} + +func handleAudioCtrlClient(conn net.Conn) { + defer conn.Close() + + scopedLogger := audioLogger.With(). + Str("addr", conn.RemoteAddr().String()). + Str("type", "audio_ctrl"). + Logger() + + scopedLogger.Info().Msg("audio socket client connected") + if audioSocketConn != nil { + scopedLogger.Debug().Msg("closing existing audio socket connection") + audioSocketConn.Close() + } + + audioSocketConn = conn + + readBuf := make([]byte, 4096) + for { + n, err := conn.Read(readBuf) + if err != nil { + scopedLogger.Warn().Err(err).Msg("error reading from audio sock") + break + } + readMsg := string(readBuf[:n]) + + audioResp := CtrlResponse{} + err = json.Unmarshal([]byte(readMsg), &audioResp) + if err != nil { + scopedLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing audio sock msg") + continue + } + scopedLogger.Trace().Interface("data", audioResp).Msg("audio sock msg") + + if audioResp.Seq != 0 { + responseChan, ok := audioOngoingRequests[audioResp.Seq] + if ok { + responseChan <- &audioResp + } + } + switch audioResp.Event { + case "audio_input_state": + HandleAudioStateMessage(audioResp) + } + } + + scopedLogger.Debug().Msg("audio sock disconnected") +} + +func startAudioBinaryWithLock(binaryPath string) (*exec.Cmd, error) { + audioCmdLock.Lock() + defer audioCmdLock.Unlock() + + cmd, err := startAudioBinary(binaryPath) + if err != nil { + return nil, err + } + audioCmd = cmd + return cmd, nil +} + +func restartAudioBinary(binaryPath string) error { + time.Sleep(10 * time.Second) + // restart the binary + audioLogger.Info().Msg("restarting audio_video binary") + cmd, err := startAudioBinary(binaryPath) + if err != nil { + audioLogger.Warn().Err(err).Msg("failed to restart binary") + } + audioCmd = cmd + return err +} + +func superviseAudioBinary(binaryPath string) error { + audioCmdLock.Lock() + defer audioCmdLock.Unlock() + + if audioCmd == nil || audioCmd.Process == nil { + return restartAudioBinary(binaryPath) + } + + err := audioCmd.Wait() + + if err == nil { + audioLogger.Info().Err(err).Msg("kvm_audio binary exited with no error") + } else if exiterr, ok := err.(*exec.ExitError); ok { + audioLogger.Warn().Int("exit_code", exiterr.ExitCode()).Msg("kvm_audio binary exited with error") + } else { + audioLogger.Warn().Err(err).Msg("kvm_audio binary exited with unknown error") + } + + return restartAudioBinary(binaryPath) +} + +func ExtractAndRunAudioBin() error { + binaryPath := "/userdata/picokvm/bin/kvm_audio" + + // Make the binary executable + if err := os.Chmod(binaryPath, 0755); err != nil { + return fmt.Errorf("failed to make binary executable: %w", err) + } + // Run the binary in the background + cmd, err := startAudioBinaryWithLock(binaryPath) + if err != nil { + return fmt.Errorf("failed to start binary: %w", err) + } + + // check if the binary is still running every 10 seconds + go func() { + for { + select { + case <-appCtx.Done(): + audioLogger.Info().Msg("stopping audio binary supervisor") + return + default: + err := superviseAudioBinary(binaryPath) + if err != nil { + audioLogger.Warn().Err(err).Msg("failed to supervise audio binary") + time.Sleep(1 * time.Second) // Add a short delay to prevent rapid successive calls + } + } + } + }() + + go func() { + <-appCtx.Done() + audioLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process") + err := cmd.Process.Kill() + if err != nil { + audioLogger.Warn().Err(err).Msg("failed to kill process") + return + } + }() + + audioLogger.Info().Int("pid", cmd.Process.Pid).Msg("kvm_audio binary started") + + return nil +} diff --git a/native_display.go b/native_display.go new file mode 100644 index 0000000..b081424 --- /dev/null +++ b/native_display.go @@ -0,0 +1,275 @@ +package kvm + +import ( + "encoding/json" + "errors" + "fmt" + "net" + "os" + "os/exec" + "sync" + "time" +) + +var ( + displayCmd *exec.Cmd + displayCmdLock = &sync.Mutex{} +) + +var displaySocketConn net.Conn + +var displayOngoingRequests = make(map[int32]chan *CtrlResponse) + +var displayLock = &sync.Mutex{} + +func CallDisplayCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) { + displayLock.Lock() + defer displayLock.Unlock() + ctrlAction := CtrlAction{ + Action: action, + Seq: seq, + Params: params, + } + + responseChan := make(chan *CtrlResponse) + displayOngoingRequests[seq] = responseChan + seq++ + + jsonData, err := json.Marshal(ctrlAction) + if err != nil { + delete(displayOngoingRequests, ctrlAction.Seq) + return nil, fmt.Errorf("error marshaling ctrl action: %w", err) + } + + scopedLogger := displayLogger.With(). + Str("action", ctrlAction.Action). + Interface("params", ctrlAction.Params).Logger() + + scopedLogger.Debug().Msg("sending display ctrl action") + + err = WriteDisplayCtrlMessage(jsonData) + if err != nil { + delete(displayOngoingRequests, ctrlAction.Seq) + return nil, ErrorfL(&scopedLogger, "error writing display ctrl message", err) + } + + select { + case response := <-responseChan: + delete(displayOngoingRequests, seq) + if response.Error != "" { + return nil, ErrorfL( + &scopedLogger, + "error display response: %s", + errors.New(response.Error), + ) + } + return response, nil + case <-time.After(10 * time.Second): + close(responseChan) + delete(displayOngoingRequests, seq) + return nil, ErrorfL(&scopedLogger, "timeout waiting for response", nil) + } +} + +func WriteDisplayCtrlMessage(message []byte) error { + if displaySocketConn == nil { + return fmt.Errorf("display socket not conn ected") + } + _, err := displaySocketConn.Write(message) + return err +} + +var displayCtrlSocketListener net.Listener + +var displayCtrlClientConnected = make(chan struct{}) + +func waitDisplayCtrlClientConnected() { + <-displayCtrlClientConnected +} + +func StartDisplaySocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener { + scopedLogger := displayLogger.With(). + Str("socket_path", socketPath). + Logger() + + // Remove the socket file if it already exists + if _, err := os.Stat(socketPath); err == nil { + if err := os.Remove(socketPath); err != nil { + scopedLogger.Warn().Err(err).Msg("failed to remove existing socket file") + os.Exit(1) + } + } + + listener, err := net.Listen("unixpacket", socketPath) + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to start server") + os.Exit(1) + } + + scopedLogger.Info().Msg("server listening") + + go func() { + for { + conn, err := listener.Accept() + + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to accept socket") + continue + } + if isCtrl { + // check if the channel is closed + select { + case <-displayCtrlClientConnected: + scopedLogger.Debug().Msg("display ctrl client reconnected") + default: + close(displayCtrlClientConnected) + scopedLogger.Debug().Msg("first display ctrl socket client connected") + } + } + + //conn.Write([]byte("[handleDisplayCtrlClient]display sock test")) + go handleClient(conn) + } + }() + + return listener +} + +func StartDisplayCtrlSocketServer() { + displayCtrlSocketListener = StartDisplaySocketServer("/var/run/kvm_display.sock", handleDisplayCtrlClient, true) + displayLogger.Debug().Msg("display ctrl sock started") +} + +func handleDisplayCtrlClient(conn net.Conn) { + defer conn.Close() + + scopedLogger := displayLogger.With(). + Str("addr", conn.RemoteAddr().String()). + Str("type", "display_ctrl"). + Logger() + + scopedLogger.Info().Msg("display socket client connected") + if displaySocketConn != nil { + scopedLogger.Debug().Msg("closing existing display socket connection") + displaySocketConn.Close() + } + + displaySocketConn = conn + + readBuf := make([]byte, 4096) + for { + n, err := conn.Read(readBuf) + if err != nil { + scopedLogger.Warn().Err(err).Msg("error reading from display sock") + break + } + readMsg := string(readBuf[:n]) + + displayResp := CtrlResponse{} + err = json.Unmarshal([]byte(readMsg), &displayResp) + if err != nil { + scopedLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing display sock msg") + continue + } + scopedLogger.Trace().Interface("data", displayResp).Msg("display sock msg") + + if displayResp.Seq != 0 { + responseChan, ok := displayOngoingRequests[displayResp.Seq] + if ok { + responseChan <- &displayResp + } + } + } + + scopedLogger.Debug().Msg("display sock disconnected") +} + +func startDisplayBinaryWithLock(binaryPath string) (*exec.Cmd, error) { + displayCmdLock.Lock() + defer displayCmdLock.Unlock() + + cmd, err := startDisplayBinary(binaryPath) + if err != nil { + return nil, err + } + displayCmd = cmd + return cmd, nil +} + +func restartDisplayBinary(binaryPath string) error { + time.Sleep(10 * time.Second) + // restart the binary + displayLogger.Info().Msg("restarting display_video binary") + cmd, err := startDisplayBinary(binaryPath) + if err != nil { + displayLogger.Warn().Err(err).Msg("failed to restart binary") + } + displayCmd = cmd + return err +} + +func superviseDisplayBinary(binaryPath string) error { + displayCmdLock.Lock() + defer displayCmdLock.Unlock() + + if displayCmd == nil || displayCmd.Process == nil { + return restartDisplayBinary(binaryPath) + } + + err := displayCmd.Wait() + + if err == nil { + displayLogger.Info().Err(err).Msg("kvm_display binary exited with no error") + } else if exiterr, ok := err.(*exec.ExitError); ok { + displayLogger.Warn().Int("exit_code", exiterr.ExitCode()).Msg("kvm_display binary exited with error") + } else { + displayLogger.Warn().Err(err).Msg("kvm_display binary exited with unknown error") + } + + return restartDisplayBinary(binaryPath) +} + +func ExtractAndRunDisplayBin() error { + binaryPath := "/userdata/picokvm/bin/kvm_display" + + // Make the binary executable + if err := os.Chmod(binaryPath, 0755); err != nil { + return fmt.Errorf("failed to make binary executable: %w", err) + } + // Run the binary in the background + cmd, err := startDisplayBinaryWithLock(binaryPath) + if err != nil { + return fmt.Errorf("failed to start binary: %w", err) + } + + // check if the binary is still running every 10 seconds + go func() { + for { + select { + case <-appCtx.Done(): + displayLogger.Info().Msg("stopping display binary supervisor") + return + default: + err := superviseDisplayBinary(binaryPath) + if err != nil { + displayLogger.Warn().Err(err).Msg("failed to supervise display binary") + time.Sleep(1 * time.Second) // Add a short delay to prevent rapid successive calls + } + } + } + }() + + go func() { + <-appCtx.Done() + displayLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process") + err := cmd.Process.Kill() + if err != nil { + displayLogger.Warn().Err(err).Msg("failed to kill process") + return + } + }() + + displayLogger.Info().Int("pid", cmd.Process.Pid).Msg("kvm_display binary started") + + return nil +} diff --git a/native_linux.go b/native_linux.go index 54d2150..f0a5b84 100644 --- a/native_linux.go +++ b/native_linux.go @@ -24,23 +24,119 @@ func (w *nativeOutput) Write(p []byte) (n int, err error) { return len(p), nil } -func startNativeBinary(binaryPath string) (*exec.Cmd, error) { - // Run the binary in the background +func startVideoBinary(binaryPath string) (*exec.Cmd, error) { + // Run the binary inthe background cmd := exec.Command(binaryPath) - nativeOutputLock := sync.Mutex{} - nativeStdout := &nativeOutput{ - mu: &nativeOutputLock, - logger: nativeLogger.Info().Str("pipe", "stdout"), + vidoeOutputLock := sync.Mutex{} + videoStdout := &nativeOutput{ + mu: &vidoeOutputLock, + logger: videoLogger.Info().Str("pipe", "stdout"), } - nativeStderr := &nativeOutput{ - mu: &nativeOutputLock, - logger: nativeLogger.Info().Str("pipe", "stderr"), + videoStderr := &nativeOutput{ + mu: &vidoeOutputLock, + logger: videoLogger.Info().Str("pipe", "stderr"), } // Redirect stdout and stderr to the current process - cmd.Stdout = nativeStdout - cmd.Stderr = nativeStderr + cmd.Stdout = videoStdout + cmd.Stderr = videoStderr + + // 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 +} + +func startAudioBinary(binaryPath string) (*exec.Cmd, error) { + // Run the binary inthe background + cmd := exec.Command(binaryPath) + + audioOutputLock := sync.Mutex{} + audioStdout := &nativeOutput{ + mu: &audioOutputLock, + logger: audioLogger.Info().Str("pipe", "stdout"), + } + audioStderr := &nativeOutput{ + mu: &audioOutputLock, + logger: audioLogger.Info().Str("pipe", "stderr"), + } + + // Redirect stdout and stderr to the current process + cmd.Stdout = audioStdout + cmd.Stderr = audioStderr + + // 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 +} + +func startVpnBinary(binaryPath string) (*exec.Cmd, error) { + // Run the binary inthe background + cmd := exec.Command(binaryPath) + + vpnOutputLock := sync.Mutex{} + vpnStdout := &nativeOutput{ + mu: &vpnOutputLock, + logger: audioLogger.Info().Str("pipe", "stdout"), + } + vpnStderr := &nativeOutput{ + mu: &vpnOutputLock, + logger: audioLogger.Info().Str("pipe", "stderr"), + } + + // Redirect stdout and stderr to the current process + cmd.Stdout = vpnStdout + cmd.Stderr = vpnStderr + + // 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 +} + +func startDisplayBinary(binaryPath string) (*exec.Cmd, error) { + // Run the binary inthe background + cmd := exec.Command(binaryPath) + + displayOutputLock := sync.Mutex{} + displayStdout := &nativeOutput{ + mu: &displayOutputLock, + logger: displayLogger.Info().Str("pipe", "stdout"), + } + displayStderr := &nativeOutput{ + mu: &displayOutputLock, + logger: displayLogger.Info().Str("pipe", "stderr"), + } + + //// Redirect stdout and stderr to the current process + cmd.Stdout = displayStdout + cmd.Stderr = displayStderr // Set the process group ID so we can kill the process and its children when this process exits cmd.SysProcAttr = &syscall.SysProcAttr{ diff --git a/native_notlinux.go b/native_notlinux.go index df6df74..2a1f8d8 100644 --- a/native_notlinux.go +++ b/native_notlinux.go @@ -7,6 +7,18 @@ import ( "os/exec" ) -func startNativeBinary(binaryPath string) (*exec.Cmd, error) { +func startVideoBinary(binaryPath string) (*exec.Cmd, error) { + return nil, fmt.Errorf("not supported") +} + +func startAudioBinary(binaryPath string) (*exec.Cmd, error) { + return nil, fmt.Errorf("not supported") +} + +func startVpnBinary(binaryPath string) (*exec.Cmd, error) { + return nil, fmt.Errorf("not supported") +} + +func startDisplayBinary(binaryPath string) (*exec.Cmd, error) { return nil, fmt.Errorf("not supported") } diff --git a/native_vpn.go b/native_vpn.go new file mode 100644 index 0000000..923305b --- /dev/null +++ b/native_vpn.go @@ -0,0 +1,279 @@ +package kvm + +import ( + "encoding/json" + "errors" + "fmt" + "net" + "os" + "os/exec" + "sync" + "time" +) + +var ( + vpnCmd *exec.Cmd + vpnCmdLock = &sync.Mutex{} +) + +var vpnSocketConn net.Conn + +var vpnOngoingRequests = make(map[int32]chan *CtrlResponse) + +var vpnLock = &sync.Mutex{} + +func CallVpnCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) { + vpnLock.Lock() + defer vpnLock.Unlock() + ctrlAction := CtrlAction{ + Action: action, + Seq: seq, + Params: params, + } + + responseChan := make(chan *CtrlResponse) + vpnOngoingRequests[seq] = responseChan + seq++ + + jsonData, err := json.Marshal(ctrlAction) + if err != nil { + delete(vpnOngoingRequests, ctrlAction.Seq) + return nil, fmt.Errorf("error marshaling ctrl action: %w", err) + } + + scopedLogger := vpnLogger.With(). + Str("action", ctrlAction.Action). + Interface("params", ctrlAction.Params).Logger() + + scopedLogger.Debug().Msg("sending vpn ctrl action") + + err = WriteVpnCtrlMessage(jsonData) + if err != nil { + delete(vpnOngoingRequests, ctrlAction.Seq) + return nil, ErrorfL(&scopedLogger, "error writing vpn ctrl message", err) + } + + select { + case response := <-responseChan: + delete(vpnOngoingRequests, seq) + if response.Error != "" { + return nil, ErrorfL( + &scopedLogger, + "error vpn response: %s", + errors.New(response.Error), + ) + } + return response, nil + case <-time.After(10 * time.Second): + close(responseChan) + delete(vpnOngoingRequests, seq) + return nil, ErrorfL(&scopedLogger, "timeout waiting for response", nil) + } +} + +func WriteVpnCtrlMessage(message []byte) error { + if vpnSocketConn == nil { + return fmt.Errorf("vpn socket not conn ected") + } + _, err := vpnSocketConn.Write(message) + return err +} + +var vpnCtrlSocketListener net.Listener + +var vpnCtrlClientConnected = make(chan struct{}) + +func waitVpnCtrlClientConnected() { + <-vpnCtrlClientConnected +} + +func StartVpnSocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener { + scopedLogger := vpnLogger.With(). + Str("socket_path", socketPath). + Logger() + + // Remove the socket file if it already exists + if _, err := os.Stat(socketPath); err == nil { + if err := os.Remove(socketPath); err != nil { + scopedLogger.Warn().Err(err).Msg("failed to remove existing socket file") + os.Exit(1) + } + } + + listener, err := net.Listen("unixpacket", socketPath) + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to start server") + os.Exit(1) + } + + scopedLogger.Info().Msg("server listening") + + go func() { + for { + conn, err := listener.Accept() + + if err != nil { + scopedLogger.Warn().Err(err).Msg("failed to accept socket") + continue + } + if isCtrl { + // check if the channel is closed + select { + case <-vpnCtrlClientConnected: + scopedLogger.Debug().Msg("vpn ctrl client reconnected") + default: + close(vpnCtrlClientConnected) + scopedLogger.Debug().Msg("first vpn ctrl socket client connected") + } + } + + //conn.Write([]byte("[handleVpnCtrlClient]vpn sock test")) + go handleClient(conn) + } + }() + + return listener +} + +func StartVpnCtrlSocketServer() { + vpnCtrlSocketListener = StartVpnSocketServer("/var/run/kvm_vpn.sock", handleVpnCtrlClient, true) + vpnLogger.Debug().Msg("vpn ctrl sock started") +} + +func handleVpnCtrlClient(conn net.Conn) { + defer conn.Close() + + scopedLogger := vpnLogger.With(). + Str("addr", conn.RemoteAddr().String()). + Str("type", "vpn_ctrl"). + Logger() + + scopedLogger.Info().Msg("vpn socket client connected") + if vpnSocketConn != nil { + scopedLogger.Debug().Msg("closing existing vpn socket connection") + vpnSocketConn.Close() + } + + vpnSocketConn = conn + + readBuf := make([]byte, 4096) + for { + n, err := conn.Read(readBuf) + if err != nil { + scopedLogger.Warn().Err(err).Msg("error reading from vpn sock") + break + } + readMsg := string(readBuf[:n]) + + vpnResp := CtrlResponse{} + err = json.Unmarshal([]byte(readMsg), &vpnResp) + if err != nil { + scopedLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing vpn sock msg") + continue + } + scopedLogger.Trace().Interface("data", vpnResp).Msg("vpn sock msg") + + if vpnResp.Seq != 0 { + responseChan, ok := vpnOngoingRequests[vpnResp.Seq] + if ok { + responseChan <- &vpnResp + } + } + switch vpnResp.Event { + case "vpn_display_update": + HandleVpnDisplayUpdateMessage(vpnResp) + } + } + + scopedLogger.Debug().Msg("vpn sock disconnected") +} + +func startVpnBinaryWithLock(binaryPath string) (*exec.Cmd, error) { + vpnCmdLock.Lock() + defer vpnCmdLock.Unlock() + + cmd, err := startVpnBinary(binaryPath) + if err != nil { + return nil, err + } + vpnCmd = cmd + return cmd, nil +} + +func restartVpnBinary(binaryPath string) error { + time.Sleep(10 * time.Second) + // restart the binary + vpnLogger.Info().Msg("restarting vpn_video binary") + cmd, err := startVpnBinary(binaryPath) + if err != nil { + vpnLogger.Warn().Err(err).Msg("failed to restart binary") + } + vpnCmd = cmd + return err +} + +func superviseVpnBinary(binaryPath string) error { + vpnCmdLock.Lock() + defer vpnCmdLock.Unlock() + + if vpnCmd == nil || vpnCmd.Process == nil { + return restartVpnBinary(binaryPath) + } + + err := vpnCmd.Wait() + + if err == nil { + vpnLogger.Info().Err(err).Msg("kvm_vpn binary exited with no error") + } else if exiterr, ok := err.(*exec.ExitError); ok { + vpnLogger.Warn().Int("exit_code", exiterr.ExitCode()).Msg("kvm_vpn binary exited with error") + } else { + vpnLogger.Warn().Err(err).Msg("kvm_vpn binary exited with unknown error") + } + + return restartVpnBinary(binaryPath) +} + +func ExtractAndRunVpnBin() error { + binaryPath := "/userdata/picokvm/bin/kvm_vpn" + + // Make the binary executable + if err := os.Chmod(binaryPath, 0755); err != nil { + return fmt.Errorf("failed to make binary executable: %w", err) + } + // Run the binary in the background + cmd, err := startVpnBinaryWithLock(binaryPath) + if err != nil { + return fmt.Errorf("failed to start binary: %w", err) + } + + // check if the binary is still running every 10 seconds + go func() { + for { + select { + case <-appCtx.Done(): + vpnLogger.Info().Msg("stopping vpn binary supervisor") + return + default: + err := superviseVpnBinary(binaryPath) + if err != nil { + vpnLogger.Warn().Err(err).Msg("failed to supervise vpn binary") + time.Sleep(1 * time.Second) // Add a short delay to prevent rapid successive calls + } + } + } + }() + + go func() { + <-appCtx.Done() + vpnLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process") + err := cmd.Process.Kill() + if err != nil { + vpnLogger.Warn().Err(err).Msg("failed to kill process") + return + } + }() + + vpnLogger.Info().Int("pid", cmd.Process.Pid).Msg("kvm_vpn binary started") + + return nil +} diff --git a/network.go b/network.go index 2208a47..620d606 100644 --- a/network.go +++ b/network.go @@ -3,8 +3,10 @@ package kvm import ( "fmt" - "github.com/jetkvm/kvm/internal/network" - "github.com/jetkvm/kvm/internal/udhcpc" + "kvm/internal/network" + "kvm/internal/udhcpc" + + "github.com/guregu/null/v6" ) const ( @@ -79,7 +81,10 @@ func rpcGetNetworkState() network.RpcNetworkState { } func rpcGetNetworkSettings() network.RpcNetworkSettings { - return networkState.RpcGetNetworkSettings() + rpcSettings := networkState.RpcGetNetworkSettings() + hostname := GetHostname() + rpcSettings.Hostname = null.NewString(hostname, hostname != "") + return rpcSettings } func rpcSetNetworkSettings(settings network.RpcNetworkSettings) (*network.RpcNetworkSettings, error) { diff --git a/ota.go b/ota.go index cf97cc0..08b3fb5 100644 --- a/ota.go +++ b/ota.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "net/http" - "net/url" "os" "os/exec" "strings" @@ -35,10 +34,18 @@ type LocalMetadata struct { SystemVersion string `json:"systemVersion"` } +type RemoteMetadata struct { + AppVersion string `json:"appVersion"` + AppUrl string `json:"appUrl"` + AppHash string `json:"appHash"` + SystemUrl string `json:"systemUrl"` + SystemVersion string `json:"systemVersion"` +} + // UpdateStatus represents the current update status type UpdateStatus struct { Local *LocalMetadata `json:"local"` - Remote *UpdateMetadata `json:"remote"` + Remote *RemoteMetadata `json:"remote"` SystemUpdateAvailable bool `json:"systemUpdateAvailable"` AppUpdateAvailable bool `json:"appUpdateAvailable"` @@ -46,9 +53,20 @@ type UpdateStatus struct { Error string `json:"error,omitempty"` } -const UpdateMetadataUrl = "https://api.jetkvm.com/releases" +var UpdateMetadataUrls = []string{ + "https://api.github.com/repos/LuckfoxTECH/PicoKVM/releases/latest", + "https://api.github.com/repos/LuckfoxTECH/kvm/releases/latest", + "https://api.github.com/repos/luckfox-eng29/kvm/releases/latest", +} -var builtAppVersion = "0.1.0+dev" +var builtAppVersion = "0.0.1+dev" + +var updateSource = "github" + +func rpcSetUpdateSource(source string) error { + updateSource = source + return nil +} func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Version, err error) { appVersion, err = semver.NewVersion(builtAppVersion) @@ -69,50 +87,81 @@ func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Versio return systemVersion, appVersion, nil } -func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateMetadata, error) { - metadata := &UpdateMetadata{} +func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease bool) (*RemoteMetadata, error) { + //cmd := exec.Command("curl", "-s", UpdateMetadataUrl) + //output, err := cmd.Output() + //if err != nil { + // return nil, fmt.Errorf("failed to fetch GitHub releases: %w", err) + //} + //_ = cmd.Process.Release() + var lastErr error - updateUrl, err := url.Parse(UpdateMetadataUrl) - if err != nil { - return nil, fmt.Errorf("error parsing update metadata URL: %w", err) + for _, url := range UpdateMetadataUrls { + resp, err := http.Get(url) + if err != nil { + lastErr = fmt.Errorf("failed to fetch GitHub releases from %s: %w", url, err) + continue + } + defer resp.Body.Close() + + output, err := io.ReadAll(resp.Body) + if err != nil { + lastErr = fmt.Errorf("failed to read GitHub releases from %s: %w", url, err) + continue + } + + if strings.Contains(string(output), "404") { + lastErr = fmt.Errorf("failed to find release from %s: %w", url, err) + continue + } + + var release struct { + TagName string `json:"tag_name"` + Assets []struct { + BrowserDownloadURL string `json:"browser_download_url"` + Digest string `json:"digest"` + } `json:"assets"` + } + if err := json.Unmarshal(output, &release); err != nil { + lastErr = fmt.Errorf("failed to parse GitHub releases JSON from %s: %w", url, err) + continue + } + + appVersionRemote := release.TagName + + var updateUrl string + var appSha256 string + if len(release.Assets) > 0 { + updateUrl = release.Assets[0].BrowserDownloadURL + appSha256 = release.Assets[0].Digest + } + + // add sha256 prefix + if strings.HasPrefix(appSha256, "sha256:") { + // delete "sha256:" + appSha256 = appSha256[7:] + } + + remoteMetadata := &RemoteMetadata{ + AppUrl: updateUrl, + AppVersion: appVersionRemote, + AppHash: appSha256, + SystemUrl: "", + SystemVersion: "0.1.0", + } + + return remoteMetadata, nil } - query := updateUrl.Query() - query.Set("deviceId", deviceId) - query.Set("prerelease", fmt.Sprintf("%v", includePreRelease)) - updateUrl.RawQuery = query.Encode() - - logger.Info().Str("url", updateUrl.String()).Msg("Checking for updates") - - req, err := http.NewRequestWithContext(ctx, "GET", updateUrl.String(), nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %w", err) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - err = json.NewDecoder(resp.Body).Decode(metadata) - if err != nil { - return nil, fmt.Errorf("error decoding response: %w", err) - } - - return metadata, nil + return nil, lastErr } func downloadFile(ctx context.Context, path string, url string, downloadProgress *float32) error { - if _, err := os.Stat(path); err == nil { - if err := os.Remove(path); err != nil { - return fmt.Errorf("error removing existing file: %w", err) - } - } + //if _, err := os.Stat(path); err == nil { + // if err := os.Remove(path); err != nil { + // return fmt.Errorf("error removing existing file: %w", err) + // } + //} unverifiedPath := path + ".unverified" if _, err := os.Stat(unverifiedPath); err == nil { @@ -127,6 +176,10 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress } defer file.Close() + if updateSource == "gitee" { + url = strings.Replace(url, "github", "gitee", 1) + } + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return fmt.Errorf("error creating request: %w", err) @@ -198,6 +251,15 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress return fmt.Errorf("error clearing filesystem caches: %w", err) } + // without check + //if err := os.Rename(unverifiedPath, path); err != nil { + // return fmt.Errorf("error renaming file: %w", err) + //} + + //if err := os.Chmod(path, 0755); err != nil { + // return fmt.Errorf("error making file executable: %w", err) + //} + return nil } @@ -345,7 +407,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err Str("remote", remote.AppVersion). Msg("App update available") - err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress) + err := downloadFile(ctx, "/userdata/picokvm/bin/kvm_app", remote.AppUrl, &otaState.AppDownloadProgress) if err != nil { otaState.Error = fmt.Sprintf("Error downloading app update: %v", err) scopedLogger.Error().Err(err).Msg("Error downloading app update") @@ -358,7 +420,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err triggerOTAStateUpdate() err = verifyFile( - "/userdata/jetkvm/jetkvm_app.update", + "/userdata/picokvm/bin/kvm_app", remote.AppHash, &otaState.AppVerificationProgress, &scopedLogger, @@ -388,7 +450,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err Str("remote", remote.SystemVersion). Msg("System update available") - err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress) + err := downloadFile(ctx, "/userdata/picokvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress) if err != nil { otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) scopedLogger.Error().Err(err).Msg("Error downloading system update") @@ -400,18 +462,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err otaState.SystemDownloadProgress = 1 triggerOTAStateUpdate() - err = verifyFile( - "/userdata/jetkvm/update_system.tar", - remote.SystemHash, - &otaState.SystemVerificationProgress, - &scopedLogger, - ) - if err != nil { - otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err) - scopedLogger.Error().Err(err).Msg("Error verifying system update hash") - triggerOTAStateUpdate() - return err - } scopedLogger.Info().Msg("System update downloaded") verifyFinished := time.Now() otaState.SystemVerifiedAt = &verifyFinished @@ -419,7 +469,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err 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/picokvm/update_system.tar", "--save_dir=/userdata/picokvm/ota_save", "--partition=all") var b bytes.Buffer cmd.Stdout = &b cmd.Stderr = &b diff --git a/prometheus.go b/prometheus.go index 5d4c5e7..da525fe 100644 --- a/prometheus.go +++ b/prometheus.go @@ -9,5 +9,5 @@ import ( func initPrometheus() { // A Prometheus metrics endpoint. version.Version = builtAppVersion - prometheus.MustRegister(versioncollector.NewCollector("jetkvm")) + prometheus.MustRegister(versioncollector.NewCollector("kvm")) } diff --git a/publish_source.sh b/publish_source.sh deleted file mode 100755 index e5c133d..0000000 --- a/publish_source.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -# Check if a commit message was provided -if [ -z "$1" ]; then - echo "Usage: $0 \"Your commit message here\"" - exit 1 -fi - -COMMIT_MESSAGE="$1" - -# Ensure you're on the main branch -git checkout main - -# Add 'public' remote if it doesn't exist -if ! git remote | grep -q '^public$'; then - git remote add public https://github.com/jetkvm/kvm.git -fi - -# Fetch the latest from the public repository -git fetch public || true - -# Create a temporary branch for the release -git checkout -b release-temp - -# If public/main exists, reset to it; else, use the root commit -if git ls-remote --heads public main | grep -q 'refs/heads/main'; then - git reset --soft public/main -else - git reset --soft "$(git rev-list --max-parents=0 HEAD)" -fi - -# Merge changes from main -git merge --squash main - -# Commit all changes as a single release commit -git commit -m "$COMMIT_MESSAGE" - -# Force push the squashed commit to the public repository -git push --force public release-temp:main - -# Switch back to main and delete the temporary branch -git checkout main -git branch -D release-temp - -# Remove the public remote -git remote remove public diff --git a/resource/embed.go b/resource/embed.go index f6e8e04..c551d4e 100644 --- a/resource/embed.go +++ b/resource/embed.go @@ -4,5 +4,4 @@ import ( "embed" ) -//go:embed jetkvm_native jetkvm_native.sha256 netboot.xyz-multiarch.iso var ResourceFS embed.FS diff --git a/serial.go b/serial.go index f4dd5b5..e6930fa 100644 --- a/serial.go +++ b/serial.go @@ -1,215 +1,16 @@ package kvm import ( - "bufio" "io" - "strconv" - "strings" - "time" "github.com/pion/webrtc/v4" "go.bug.st/serial" ) -const serialPortPath = "/dev/ttyS3" +const serialPortPath = "/dev/ttyS0" var port serial.Port -func mountATXControl() error { - _ = port.SetMode(defaultMode) - go runATXControl() - - return nil -} - -func unmountATXControl() error { - _ = reopenSerialPort() - return nil -} - -var ( - ledHDDState bool - ledPWRState bool - btnRSTState bool - btnPWRState bool -) - -func runATXControl() { - scopedLogger := serialLogger.With().Str("service", "atx_control").Logger() - - reader := bufio.NewReader(port) - for { - line, err := reader.ReadString('\n') - if err != nil { - scopedLogger.Warn().Err(err).Msg("Error reading from serial port") - return - } - - // Each line should be 4 binary digits + newline - if len(line) != 5 { - scopedLogger.Warn().Int("length", len(line)).Msg("Invalid line length") - continue - } - - // Parse new states - newLedHDDState := line[0] == '0' - newLedPWRState := line[1] == '0' - newBtnRSTState := line[2] == '1' - newBtnPWRState := line[3] == '1' - - if currentSession != nil { - writeJSONRPCEvent("atxState", ATXState{ - Power: newLedPWRState, - HDD: newLedHDDState, - }, currentSession) - } - - if newLedHDDState != ledHDDState || - newLedPWRState != ledPWRState || - newBtnRSTState != btnRSTState || - newBtnPWRState != btnPWRState { - scopedLogger.Debug(). - Bool("hdd", newLedHDDState). - Bool("pwr", newLedPWRState). - Bool("rst", newBtnRSTState). - Bool("pwr", newBtnPWRState). - Msg("Status changed") - - // Update states - ledHDDState = newLedHDDState - ledPWRState = newLedPWRState - btnRSTState = newBtnRSTState - btnPWRState = newBtnPWRState - } - } -} - -func pressATXPowerButton(duration time.Duration) error { - _, err := port.Write([]byte("\n")) - if err != nil { - return err - } - - _, err = port.Write([]byte("BTN_PWR_ON\n")) - if err != nil { - return err - } - - time.Sleep(duration) - - _, err = port.Write([]byte("BTN_PWR_OFF\n")) - if err != nil { - return err - } - - return nil -} - -func pressATXResetButton(duration time.Duration) error { - _, err := port.Write([]byte("\n")) - if err != nil { - return err - } - - _, err = port.Write([]byte("BTN_RST_ON\n")) - if err != nil { - return err - } - - time.Sleep(duration) - - _, err = port.Write([]byte("BTN_RST_OFF\n")) - if err != nil { - return err - } - - return nil -} - -func mountDCControl() error { - _ = port.SetMode(defaultMode) - go runDCControl() - return nil -} - -func unmountDCControl() error { - _ = reopenSerialPort() - return nil -} - -var dcState DCPowerState - -func runDCControl() { - scopedLogger := serialLogger.With().Str("service", "dc_control").Logger() - reader := bufio.NewReader(port) - for { - line, err := reader.ReadString('\n') - if err != nil { - scopedLogger.Warn().Err(err).Msg("Error reading from serial port") - return - } - - // Split the line by semicolon - parts := strings.Split(strings.TrimSpace(line), ";") - if len(parts) != 4 { - scopedLogger.Warn().Str("line", line).Msg("Invalid line") - continue - } - - // Parse new states - powerState, err := strconv.Atoi(parts[0]) - if err != nil { - scopedLogger.Warn().Err(err).Msg("Invalid power state") - continue - } - dcState.IsOn = powerState == 1 - milliVolts, err := strconv.ParseFloat(parts[1], 64) - if err != nil { - scopedLogger.Warn().Err(err).Msg("Invalid voltage") - continue - } - volts := milliVolts / 1000 // Convert mV to V - - milliAmps, err := strconv.ParseFloat(parts[2], 64) - if err != nil { - scopedLogger.Warn().Err(err).Msg("Invalid current") - continue - } - amps := milliAmps / 1000 // Convert mA to A - - milliWatts, err := strconv.ParseFloat(parts[3], 64) - if err != nil { - scopedLogger.Warn().Err(err).Msg("Invalid power") - continue - } - watts := milliWatts / 1000 // Convert mW to W - - dcState.Voltage = volts - dcState.Current = amps - dcState.Power = watts - - if currentSession != nil { - writeJSONRPCEvent("dcState", dcState, currentSession) - } - } -} - -func setDCPowerState(on bool) error { - _, err := port.Write([]byte("\n")) - if err != nil { - return err - } - command := "PWR_OFF\n" - if on { - command = "PWR_ON\n" - } - _, err = port.Write([]byte(command)) - if err != nil { - return err - } - return nil -} - var defaultMode = &serial.Mode{ BaudRate: 115200, DataBits: 8, @@ -219,17 +20,12 @@ var defaultMode = &serial.Mode{ func initSerialPort() { _ = reopenSerialPort() - switch config.ActiveExtension { - case "atx-power": - _ = mountATXControl() - case "dc-power": - _ = mountDCControl() - } } func reopenSerialPort() error { if port != nil { port.Close() + port = nil } var err error port, err = serial.Open(serialPortPath, defaultMode) @@ -239,7 +35,12 @@ func reopenSerialPort() error { Str("path", serialPortPath). Interface("mode", defaultMode). Msg("Error opening serial port") + return err } + serialLogger.Info(). + Str("path", serialPortPath). + Interface("mode", defaultMode). + Msg("Serial port opened successfully") return nil } @@ -248,6 +49,12 @@ func handleSerialChannel(d *webrtc.DataChannel) { Uint16("data_channel_id", *d.ID()).Logger() d.OnOpen(func() { + if err := reopenSerialPort(); err != nil { + scopedLogger.Error().Err(err).Msg("Failed to open serial port") + d.Close() + return + } + go func() { buf := make([]byte, 1024) for { @@ -258,8 +65,7 @@ func handleSerialChannel(d *webrtc.DataChannel) { } break } - err = d.Send(buf[:n]) - if err != nil { + if err := d.Send(buf[:n]); err != nil { scopedLogger.Warn().Err(err).Msg("Failed to send serial output") break } @@ -282,6 +88,10 @@ func handleSerialChannel(d *webrtc.DataChannel) { }) d.OnClose(func() { + if port != nil { + port.Close() + port = nil + } scopedLogger.Info().Msg("Serial channel closed") }) } diff --git a/timesync.go b/timesync.go index 7b25fe2..1a74b8d 100644 --- a/timesync.go +++ b/timesync.go @@ -4,7 +4,7 @@ import ( "strconv" "time" - "github.com/jetkvm/kvm/internal/timesync" + "kvm/internal/timesync" ) var ( @@ -51,3 +51,11 @@ func initTimeSync() { }, }) } + +func initTimeZone() { + LoadConfig() + _, err := CallDisplayCtrlAction("set_timezone", map[string]interface{}{"timezone": config.TimeZone}) + if err != nil { + timesyncLogger.Error().Err(err).Msg("failed to set time zone") + } +} diff --git a/ui/.env.cloud-production b/ui/.env.cloud-production deleted file mode 100644 index d9895d2..0000000 --- a/ui/.env.cloud-production +++ /dev/null @@ -1,4 +0,0 @@ -# No need for VITE_CLOUD_APP it's only needed for the device build - -# We use this for all the cloud API requests from the browser -VITE_CLOUD_API=https://api.jetkvm.com diff --git a/ui/.env.cloud-staging b/ui/.env.cloud-staging deleted file mode 100644 index bc5c14c..0000000 --- a/ui/.env.cloud-staging +++ /dev/null @@ -1,4 +0,0 @@ -# No need for VITE_CLOUD_APP it's only needed for the device build - -# We use this for all the cloud API requests from the browser -VITE_CLOUD_API=https://staging-api.jetkvm.com diff --git a/ui/dev_device.sh b/ui/dev_device.sh index 2c7b497..12fe82c 100755 --- a/ui/dev_device.sh +++ b/ui/dev_device.sh @@ -2,7 +2,7 @@ # Check if an IP address was provided as an argument if [ -z "$1" ]; then - echo "Usage: $0 " + echo "Usage: $0 " exit 1 fi @@ -10,11 +10,11 @@ ip_address="$1" # Print header echo "┌──────────────────────────────────────┐" -echo "│ JetKVM Development Setup │" +echo "│ KVM Development Setup │" echo "└──────────────────────────────────────┘" # Set the environment variable and run Vite -echo "Starting development server with JetKVM device at: $ip_address" +echo "Starting development server with KVM device at: $ip_address" # Check if pwd is the current directory of the script if [ "$(pwd)" != "$(dirname "$0")" ]; then @@ -24,6 +24,6 @@ fi sleep 1 -JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device +KVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device popd > /dev/null diff --git a/ui/index.html b/ui/index.html index af9bdfb..6629bdc 100644 --- a/ui/index.html +++ b/ui/index.html @@ -25,9 +25,9 @@ type="font/woff2" crossorigin /> - JetKVM + KVM - +