diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e96e24b..571b10d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,5 +6,9 @@ // Should match what is defined in ui/package.json "version": "21.1.0" } - } + }, + "mounts": [ + "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" + ] } + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..68e1cb5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,142 @@ +name: build image +on: + push: + branches: + - dev + - main + workflow_dispatch: + pull_request_review: + types: [submitted] + +jobs: + build: + runs-on: buildjet-4vcpu-ubuntu-2204 + name: Build + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: v21.1.0 + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + - name: Set up Golang + uses: actions/setup-go@v4 + with: + go-version: '1.24.0' + - name: Build frontend + run: | + make frontend + - name: Build application + run: | + make build_dev + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: jetkvm-app + path: bin/jetkvm_app + deploy_and_test: + runs-on: buildjet-4vcpu-ubuntu-2204 + name: Smoke test + needs: build + concurrency: + group: smoketest-jk + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: jetkvm-app + - name: Configure WireGuard and check connectivity + run: | + WG_KEY_FILE=$(mktemp) + echo -n "$CI_WG_PRIVATE" > $WG_KEY_FILE && \ + sudo apt-get update && sudo apt-get install -y wireguard-tools && \ + sudo ip link add dev wg-ci type wireguard && \ + sudo ip addr add $CI_WG_IPS dev wg-ci && \ + sudo wg set wg-ci listen-port 51820 \ + private-key $WG_KEY_FILE \ + peer $CI_WG_PUBLIC \ + allowed-ips $CI_WG_ALLOWED_IPS \ + endpoint $CI_WG_ENDPOINT \ + persistent-keepalive 15 && \ + sudo ip link set up dev wg-ci && \ + 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 }} + - name: Configure SSH + run: | + # Write SSH private key to a file + SSH_PRIVATE_KEY=$(mktemp) + echo "$CI_SSH_PRIVATE" > $SSH_PRIVATE_KEY + chmod 0600 $SSH_PRIVATE_KEY + # Configure SSH + mkdir -p ~/.ssh + cat <> ~/.ssh/config + Host jkci + HostName $CI_HOST + User $CI_USER + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + 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 }} + - name: Deploy application + run: | + set -e + # Copy the binary to the remote host + echo "+ Copying the application to the remote host" + cat jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz" + # Deploy and run the application on the remote host + echo "+ Deploying the application on the remote host" + ssh jkci ash < /proc/sys/vm/drop_caches + # Reboot the application + reboot -d 5 -f & + EOF + sleep 10 + echo "Deployment complete, waiting for JetKVM to come back online " + function check_online() { + for i in {1..60}; do + if ping -c1 -w1 -W1 -q $CI_HOST >/dev/null; then + echo "JetKVM is back online" + return 0 + fi + echo -n "." + sleep 1 + done + echo "JetKVM did not come back online within 60 seconds" + return 1 + } + check_online + env: + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + - name: Run smoke tests + run: | + echo "+ Checking the status of the device" + curl -v http://$CI_HOST/device/status && echo + echo "+ Collecting logs" + ssh jkci "cat /userdata/jetkvm/last.log" > last.log + cat last.log + env: + CI_HOST: ${{ vars.JETKVM_CI_HOST }} + - name: Upload logs + uses: actions/upload-artifact@v4 + with: + name: device-logs + path: last.log diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..8c828cf --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,34 @@ +--- +name: golangci-lint +on: + push: + paths: + - "go.sum" + - "go.mod" + - "**.go" + - ".github/workflows/golangci-lint.yml" + - ".golangci.yml" + pull_request: + +permissions: # added using https://github.com/step-security/secure-repo + contents: read + +jobs: + golangci: + permissions: + contents: read # for actions/checkout to fetch code + pull-requests: read # for golangci/golangci-lint-action to fetch pull requests + name: lint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Install Go + uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version: 1.23.x + - name: Lint + uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 + with: + args: --verbose + version: v1.62.0 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..ddf4443 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,12 @@ +--- +linters: + enable: + # - goimports + # - misspell + # - revive + +issues: + exclude-rules: + - path: _test.go + linters: + - errcheck diff --git a/Makefile b/Makefile index 8aaa8df..2aefdea 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,31 @@ -VERSION_DEV := 0.3.8-dev$(shell date +%Y%m%d%H%M) -VERSION := 0.3.7 +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.3.9-dev$(shell date +%Y%m%d%H%M) +VERSION := 0.3.8 + +PROMETHEUS_TAG := github.com/prometheus/common/version +KVM_PKG_NAME := github.com/jetkvm/kvm + +GO_LDFLAGS := \ + -s -w \ + -X $(PROMETHEUS_TAG).Branch=$(BRANCH) \ + -X $(PROMETHEUS_TAG).BuildDate=$(BUILDDATE) \ + -X $(PROMETHEUS_TAG).Revision=$(REVISION) \ + -X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS) hash_resource: @shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256 build_dev: hash_resource @echo "Building..." - GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w -X kvm.builtAppVersion=$(VERSION_DEV)" -o bin/jetkvm_app cmd/main.go + GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" -o bin/jetkvm_app cmd/main.go frontend: cd ui && npm ci && npm run build:device -dev_release: build_dev +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 @@ -19,7 +33,7 @@ dev_release: build_dev build_release: frontend hash_resource @echo "Building release..." - GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w -X kvm.builtAppVersion=$(VERSION)" -o bin/jetkvm_app cmd/main.go + GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" -o bin/jetkvm_app cmd/main.go release: @if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \ diff --git a/block_device.go b/block_device.go index 1e34884..3a44135 100644 --- a/block_device.go +++ b/block_device.go @@ -3,7 +3,6 @@ package kvm import ( "context" "errors" - "log" "net" "os" "time" @@ -94,7 +93,8 @@ func (d *NBDDevice) Start() error { // Remove the socket file if it already exists if _, err := os.Stat(nbdSocketPath); err == nil { if err := os.Remove(nbdSocketPath); err != nil { - log.Fatalf("Failed to remove existing socket file %s: %v", nbdSocketPath, err) + logger.Errorf("Failed to remove existing socket file %s: %v", nbdSocketPath, err) + os.Exit(1) } } @@ -134,7 +134,7 @@ func (d *NBDDevice) runServerConn() { MaximumBlockSize: uint32(16 * 1024), SupportsMultiConn: false, }) - log.Println("nbd server exited:", err) + logger.Infof("nbd server exited: %v", err) } func (d *NBDDevice) runClientConn() { @@ -142,14 +142,14 @@ func (d *NBDDevice) runClientConn() { ExportName: "jetkvm", BlockSize: uint32(4 * 1024), }) - log.Println("nbd client exited:", err) + logger.Infof("nbd client exited: %v", err) } func (d *NBDDevice) Close() { if d.dev != nil { err := client.Disconnect(d.dev) if err != nil { - log.Println("error disconnecting nbd client:", err) + logger.Warnf("error disconnecting nbd client: %v", err) } _ = d.dev.Close() } diff --git a/cloud.go b/cloud.go index 3520e2f..a30a14c 100644 --- a/cloud.go +++ b/cloud.go @@ -24,6 +24,18 @@ type CloudRegisterRequest struct { ClientId string `json:"clientId"` } +const ( + // CloudWebSocketConnectTimeout is the timeout for the websocket connection to the cloud + CloudWebSocketConnectTimeout = 1 * time.Minute + // CloudAPIRequestTimeout is the timeout for cloud API requests + CloudAPIRequestTimeout = 10 * time.Second + // CloudOidcRequestTimeout is the timeout for OIDC token verification requests + // should be lower than the websocket response timeout set in cloud-api + CloudOidcRequestTimeout = 10 * time.Second + // CloudWebSocketPingInterval is the interval at which the websocket client sends ping messages to the cloud + CloudWebSocketPingInterval = 15 * time.Second +) + func handleCloudRegister(c *gin.Context) { var req CloudRegisterRequest @@ -44,22 +56,31 @@ func handleCloudRegister(c *gin.Context) { return } - resp, err := http.Post(req.CloudAPI+"/devices/token", "application/json", bytes.NewBuffer(jsonPayload)) + 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 resp.Body.Close() + defer apiResp.Body.Close() - if resp.StatusCode != http.StatusOK { - c.JSON(resp.StatusCode, gin.H{"error": "Failed to exchange token: " + resp.Status}) + 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(resp.Body).Decode(&tokenResp); err != nil { + if err := json.NewDecoder(apiResp.Body).Decode(&tokenResp); err != nil { c.JSON(500, gin.H{"error": "Failed to parse token response: " + err.Error()}) return } @@ -70,12 +91,11 @@ func handleCloudRegister(c *gin.Context) { } if config.CloudToken == "" { - logger.Info("Starting websocket client due to adoption") + cloudLogger.Info("Starting websocket client due to adoption") go RunWebsocketClient() } config.CloudToken = tokenResp.SecretToken - config.CloudURL = req.CloudAPI provider, err := oidc.NewProvider(c, "https://accounts.google.com") if err != nil { @@ -122,7 +142,7 @@ func runWebsocketClient() error { header := http.Header{} header.Set("X-Device-ID", GetDeviceID()) header.Set("Authorization", "Bearer "+config.CloudToken) - dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Minute) + dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout) defer cancelDial() c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ HTTPHeader: header, @@ -130,16 +150,16 @@ func runWebsocketClient() error { if err != nil { return err } - defer c.CloseNow() - logger.Infof("WS connected to %v", wsURL.String()) + defer c.CloseNow() //nolint:errcheck + cloudLogger.Infof("websocket connected to %s", wsURL) runCtx, cancelRun := context.WithCancel(context.Background()) defer cancelRun() go func() { for { - time.Sleep(15 * time.Second) + time.Sleep(CloudWebSocketPingInterval) err := c.Ping(runCtx) if err != nil { - logger.Warnf("websocket ping error: %v", err) + cloudLogger.Warnf("websocket ping error: %v", err) cancelRun() return } @@ -157,24 +177,30 @@ func runWebsocketClient() error { var req WebRTCSessionRequest err = json.Unmarshal(msg, &req) if err != nil { - logger.Warnf("unable to parse ws message: %v", string(msg)) + cloudLogger.Warnf("unable to parse ws message: %v", string(msg)) continue } + cloudLogger.Infof("new session request: %v", req.OidcGoogle) + cloudLogger.Tracef("session request info: %v", req) + err = handleSessionRequest(runCtx, c, req) if err != nil { - logger.Infof("error starting new session: %v", err) + cloudLogger.Infof("error starting new session: %v", err) continue } } } func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { - oidcCtx, cancelOIDC := context.WithTimeout(ctx, time.Minute) + oidcCtx, cancelOIDC := context.WithTimeout(ctx, CloudOidcRequestTimeout) defer cancelOIDC() provider, err := oidc.NewProvider(oidcCtx, "https://accounts.google.com") if err != nil { - fmt.Println("Failed to initialize OIDC provider:", err) + _ = wsjson.Write(context.Background(), c, gin.H{ + "error": fmt.Sprintf("failed to initialize OIDC provider: %v", err), + }) + cloudLogger.Errorf("failed to initialize OIDC provider: %v", err) return err } @@ -190,6 +216,7 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess 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") } @@ -216,6 +243,9 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess _ = peerConn.Close() }() } + + cloudLogger.Info("new session accepted") + cloudLogger.Tracef("new session accepted: %v", session) currentSession = session _ = wsjson.Write(context.Background(), c, gin.H{"sd": sd}) return nil @@ -225,7 +255,7 @@ func RunWebsocketClient() { for { err := runWebsocketClient() if err != nil { - fmt.Println("Websocket client error:", err) + cloudLogger.Errorf("websocket client error: %v", err) time.Sleep(5 * time.Second) } } @@ -234,12 +264,14 @@ func RunWebsocketClient() { 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, } } @@ -254,7 +286,7 @@ func rpcDeregisterDevice() error { } req.Header.Set("Authorization", "Bearer "+config.CloudToken) - client := &http.Client{Timeout: 10 * time.Second} + client := &http.Client{Timeout: CloudAPIRequestTimeout} resp, err := client.Do(req) if err != nil { return fmt.Errorf("failed to send deregister request: %w", err) @@ -267,8 +299,8 @@ func rpcDeregisterDevice() error { // (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.CloudURL = "" config.GoogleIdentity = "" + if err := SaveConfig(); err != nil { return fmt.Errorf("failed to save configuration after deregistering: %w", err) } diff --git a/cmd/main.go b/cmd/main.go index 6080aff..ab44ac9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,7 +1,7 @@ package main import ( - "kvm" + "github.com/jetkvm/kvm" ) func main() { diff --git a/config.go b/config.go index 87301a8..642f113 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,8 @@ import ( "fmt" "os" "sync" + + "github.com/jetkvm/kvm/internal/usbgadget" ) type WakeOnLanDevice struct { @@ -13,32 +15,51 @@ type WakeOnLanDevice struct { } type Config struct { - CloudURL string `json:"cloud_url"` - CloudToken string `json:"cloud_token"` - GoogleIdentity string `json:"google_identity"` - JigglerEnabled bool `json:"jiggler_enabled"` - AutoUpdateEnabled bool `json:"auto_update_enabled"` - IncludePreRelease bool `json:"include_pre_release"` - HashedPassword string `json:"hashed_password"` - LocalAuthToken string `json:"local_auth_token"` - LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration - WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` - EdidString string `json:"hdmi_edid_string"` - ActiveExtension string `json:"active_extension"` - DisplayMaxBrightness int `json:"display_max_brightness"` - DisplayDimAfterSec int `json:"display_dim_after_sec"` - DisplayOffAfterSec int `json:"display_off_after_sec"` + 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"` + AutoUpdateEnabled bool `json:"auto_update_enabled"` + IncludePreRelease bool `json:"include_pre_release"` + HashedPassword string `json:"hashed_password"` + LocalAuthToken string `json:"local_auth_token"` + LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration + WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + EdidString string `json:"hdmi_edid_string"` + ActiveExtension string `json:"active_extension"` + DisplayMaxBrightness int `json:"display_max_brightness"` + DisplayDimAfterSec int `json:"display_dim_after_sec"` + DisplayOffAfterSec int `json:"display_off_after_sec"` + TLSMode string `json:"tls_mode"` + UsbConfig *usbgadget.Config `json:"usb_config"` + UsbDevices *usbgadget.Devices `json:"usb_devices"` } 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 ActiveExtension: "", DisplayMaxBrightness: 64, DisplayDimAfterSec: 120, // 2 minutes DisplayOffAfterSec: 1800, // 30 minutes + TLSMode: "", + UsbConfig: &usbgadget.Config{ + VendorId: "0x1d6b", //The Linux Foundation + ProductId: "0x0104", //Multifunction Composite Gadget + SerialNumber: "", + Manufacturer: "JetKVM", + Product: "USB Emulation Device", + }, + UsbDevices: &usbgadget.Devices{ + AbsoluteMouse: true, + RelativeMouse: true, + Keyboard: true, + MassStorage: true, + }, } var ( @@ -47,6 +68,9 @@ var ( ) func LoadConfig() { + configLock.Lock() + defer configLock.Unlock() + if config != nil { logger.Info("config already loaded, skipping") return @@ -69,6 +93,15 @@ func LoadConfig() { return } + // merge the user config with the default config + if loadedConfig.UsbConfig == nil { + loadedConfig.UsbConfig = defaultConfig.UsbConfig + } + + if loadedConfig.UsbDevices == nil { + loadedConfig.UsbDevices = defaultConfig.UsbDevices + } + config = &loadedConfig } @@ -90,3 +123,9 @@ func SaveConfig() error { return nil } + +func ensureConfigLoaded() { + if config == nil { + LoadConfig() + } +} diff --git a/dev_deploy.sh b/dev_deploy.sh index a106395..7fbf29e 100755 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -10,6 +10,7 @@ show_help() { echo echo "Optional:" echo " -u, --user Remote username (default: root)" + echo " --skip-ui-build Skip frontend/UI build" echo " --help Display this help message" echo echo "Example:" @@ -21,6 +22,7 @@ show_help() { # Default values REMOTE_USER="root" REMOTE_PATH="/userdata/jetkvm/bin" +SKIP_UI_BUILD=false # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -33,6 +35,10 @@ while [[ $# -gt 0 ]]; do REMOTE_USER="$2" shift 2 ;; + --skip-ui-build) + SKIP_UI_BUILD=true + shift + ;; --help) show_help exit 0 @@ -52,12 +58,17 @@ if [ -z "$REMOTE_HOST" ]; then fi # Build the development version on the host -make frontend +if [ "$SKIP_UI_BUILD" = false ]; then + make frontend +fi make build_dev # Change directory to the binary output directory cd bin +# Kill any existing instances of the application +ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" + # Copy the binary to the remote host cat jetkvm_app | ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > $REMOTE_PATH/jetkvm_app_debug" @@ -79,8 +90,7 @@ cd "$REMOTE_PATH" chmod +x jetkvm_app_debug # Run the application in the background -./jetkvm_app_debug - +PION_LOG_TRACE=jetkvm,cloud ./jetkvm_app_debug EOF echo "Deployment complete." diff --git a/display.go b/display.go index 9d22c26..f4e8cf7 100644 --- a/display.go +++ b/display.go @@ -3,7 +3,6 @@ package kvm import ( "errors" "fmt" - "log" "os" "strconv" "time" @@ -25,7 +24,7 @@ const ( func switchToScreen(screen string) { _, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen}) if err != nil { - log.Printf("failed to switch to screen %s: %v", screen, err) + logger.Warnf("failed to switch to screen %s: %v", screen, err) return } currentScreen = screen @@ -41,7 +40,7 @@ func updateLabelIfChanged(objName string, newText string) { } func switchToScreenIfDifferent(screenName string) { - fmt.Println("switching screen from", currentScreen, screenName) + logger.Infof("switching screen from %s to %s", currentScreen, screenName) if currentScreen != screenName { switchToScreen(screenName) } @@ -75,12 +74,12 @@ var displayInited = false func requestDisplayUpdate() { if !displayInited { - fmt.Println("display not inited, skipping updates") + logger.Info("display not inited, skipping updates") return } go func() { wakeDisplay(false) - fmt.Println("display updating........................") + logger.Info("display updating") //TODO: only run once regardless how many pending updates updateDisplay() }() @@ -119,7 +118,7 @@ func setDisplayBrightness(brightness int) error { return err } - fmt.Printf("display: set brightness to %v\n", brightness) + logger.Infof("display: set brightness to %v", brightness) return nil } @@ -128,7 +127,7 @@ func setDisplayBrightness(brightness int) error { func tick_displayDim() { err := setDisplayBrightness(config.DisplayMaxBrightness / 2) if err != nil { - fmt.Printf("display: failed to dim display: %s\n", err) + logger.Warnf("display: failed to dim display: %s", err) } dimTicker.Stop() @@ -141,7 +140,7 @@ func tick_displayDim() { func tick_displayOff() { err := setDisplayBrightness(0) if err != nil { - fmt.Printf("display: failed to turn off display: %s\n", err) + logger.Warnf("display: failed to turn off display: %s", err) } offTicker.Stop() @@ -164,7 +163,7 @@ func wakeDisplay(force bool) { err := setDisplayBrightness(config.DisplayMaxBrightness) if err != nil { - fmt.Printf("display wake failed, %s\n", err) + logger.Warnf("display wake failed, %s", err) } if config.DisplayDimAfterSec != 0 { @@ -184,7 +183,7 @@ func wakeDisplay(force bool) { func watchTsEvents() { ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666) if err != nil { - fmt.Printf("display: failed to open touchscreen device: %s\n", err) + logger.Warnf("display: failed to open touchscreen device: %s", err) return } @@ -197,7 +196,7 @@ func watchTsEvents() { for { _, err := ts.Read(buf) if err != nil { - fmt.Printf("display: failed to read from touchscreen device: %s\n", err) + logger.Warnf("display: failed to read from touchscreen device: %s", err) return } @@ -212,17 +211,17 @@ func startBacklightTickers() { // Don't start the tickers if the display is switched off. // Set the display to off if that's the case. if config.DisplayMaxBrightness == 0 { - setDisplayBrightness(0) + _ = setDisplayBrightness(0) return } if dimTicker == nil && config.DisplayDimAfterSec != 0 { - fmt.Printf("display: dim_ticker has started\n") + logger.Info("display: dim_ticker has started") dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second) defer dimTicker.Stop() go func() { - for { + for { //nolint:gosimple select { case <-dimTicker.C: tick_displayDim() @@ -232,12 +231,12 @@ func startBacklightTickers() { } if offTicker == nil && config.DisplayOffAfterSec != 0 { - fmt.Printf("display: off_ticker has started\n") + logger.Info("display: off_ticker has started") offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second) defer offTicker.Stop() go func() { - for { + for { //nolint:gosimple select { case <-offTicker.C: tick_displayOff() @@ -248,13 +247,15 @@ func startBacklightTickers() { } func init() { + ensureConfigLoaded() + go func() { waitCtrlClientConnected() - fmt.Println("setting initial display contents") + logger.Info("setting initial display contents") time.Sleep(500 * time.Millisecond) updateStaticContents() displayInited = true - fmt.Println("display inited") + logger.Info("display inited") startBacklightTickers() wakeDisplay(true) requestDisplayUpdate() diff --git a/fuse.go b/fuse.go index 6ecc49c..29b11f7 100644 --- a/fuse.go +++ b/fuse.go @@ -2,7 +2,6 @@ package kvm import ( "context" - "fmt" "os" "sync" "syscall" @@ -104,7 +103,7 @@ func RunFuseServer() { var err error fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts) if err != nil { - fmt.Println("failed to mount fuse: %w", err) + logger.Warnf("failed to mount fuse: %v", err) } fuseServer.Wait() } diff --git a/go.mod b/go.mod index adc054a..93fedab 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module kvm +module github.com/jetkvm/kvm go 1.21.0 @@ -15,23 +15,26 @@ require ( github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf github.com/hanwen/go-fuse/v2 v2.5.1 github.com/hashicorp/go-envparse v0.1.0 - github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965 github.com/pion/logging v0.2.2 github.com/pion/mdns/v2 v2.0.7 github.com/pion/webrtc/v4 v4.0.0 github.com/pojntfx/go-nbd v0.3.2 + github.com/prometheus/client_golang v1.21.0 + github.com/prometheus/common v0.62.0 github.com/psanford/httpreadat v0.1.0 github.com/vishvananda/netlink v1.3.0 go.bug.st/serial v1.6.2 - golang.org/x/crypto v0.28.0 - golang.org/x/net v0.30.0 + golang.org/x/crypto v0.31.0 + golang.org/x/net v0.33.0 ) replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b require ( + github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/creack/goselect v0.1.2 // indirect @@ -43,12 +46,13 @@ require ( github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/kr/pretty v0.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pilebones/go-udev v0.9.0 // indirect github.com/pion/datachannel v1.5.9 // indirect @@ -64,16 +68,16 @@ require ( github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v4 v4.0.0 // indirect - github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.19.0 // indirect - google.golang.org/protobuf v1.34.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.36.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b7b8756..b5769d8 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,14 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+ github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM= github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs= github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= @@ -18,7 +22,6 @@ github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/ github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -42,8 +45,8 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -55,20 +58,22 @@ github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdm github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/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.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -80,8 +85,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965 h1:bZGtUfkOl0dqvem8ltx9KCYied0gSlRuDhaZDxgppN4= -github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965/go.mod h1:6cAIK2c4O3/yETSrRjmNwsBL3yE4Vcu9M9p/Qwx5+gM= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q= @@ -118,14 +123,20 @@ github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8= github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= +github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE= github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -136,8 +147,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= @@ -153,29 +165,27 @@ go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= -google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hw.go b/hw.go index efe8f5c..03e9d4b 100644 --- a/hw.go +++ b/hw.go @@ -14,7 +14,7 @@ func extractSerialNumber() (string, error) { return "", err } - r, err := regexp.Compile("Serial\\s*:\\s*(\\S+)") + r, err := regexp.Compile(`Serial\s*:\s*(\S+)`) if err != nil { return "", fmt.Errorf("failed to compile regex: %w", err) } @@ -27,7 +27,7 @@ func extractSerialNumber() (string, error) { return matches[1], nil } -func readOtpEntropy() ([]byte, error) { +func readOtpEntropy() ([]byte, error) { //nolint:unused content, err := os.ReadFile("/sys/bus/nvmem/devices/rockchip-otp0/nvmem") if err != nil { return nil, err diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go new file mode 100644 index 0000000..5cc3ed2 --- /dev/null +++ b/internal/usbgadget/config.go @@ -0,0 +1,336 @@ +package usbgadget + +import ( + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "sort" +) + +type gadgetConfigItem struct { + order uint + device string + path []string + attrs gadgetAttributes + configAttrs gadgetAttributes + configPath []string + reportDesc []byte +} + +type gadgetAttributes map[string]string + +type gadgetConfigItemWithKey struct { + key string + item gadgetConfigItem +} + +type orderedGadgetConfigItems []gadgetConfigItemWithKey + +var defaultGadgetConfig = map[string]gadgetConfigItem{ + "base": { + order: 0, + attrs: gadgetAttributes{ + "bcdUSB": "0x0200", // USB 2.0 + "idVendor": "0x1d6b", // The Linux Foundation + "idProduct": "0104", // Multifunction Composite Gadget + "bcdDevice": "0100", + }, + configAttrs: gadgetAttributes{ + "MaxPower": "250", // in unit of 2mA + }, + }, + "base_info": { + order: 1, + path: []string{"strings", "0x409"}, + configPath: []string{"strings", "0x409"}, + attrs: gadgetAttributes{ + "serialnumber": "", + "manufacturer": "JetKVM", + "product": "JetKVM USB Emulation Device", + }, + configAttrs: gadgetAttributes{ + "configuration": "Config 1: HID", + }, + }, + // keyboard HID + "keyboard": keyboardConfig, + // mouse HID + "absolute_mouse": absoluteMouseConfig, + // relative mouse HID + "relative_mouse": relativeMouseConfig, + // mass storage + "mass_storage_base": massStorageBaseConfig, + "mass_storage_lun0": massStorageLun0Config, +} + +func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool { + switch itemKey { + case "absolute_mouse": + return u.enabledDevices.AbsoluteMouse + case "relative_mouse": + return u.enabledDevices.RelativeMouse + case "keyboard": + return u.enabledDevices.Keyboard + case "mass_storage_base": + return u.enabledDevices.MassStorage + case "mass_storage_lun0": + return u.enabledDevices.MassStorage + default: + return true + } +} + +func (u *UsbGadget) loadGadgetConfig() { + if u.customConfig.isEmpty { + u.log.Trace("using default gadget config") + return + } + + u.configMap["base"].attrs["idVendor"] = u.customConfig.VendorId + u.configMap["base"].attrs["idProduct"] = u.customConfig.ProductId + + u.configMap["base_info"].attrs["serialnumber"] = u.customConfig.SerialNumber + u.configMap["base_info"].attrs["manufacturer"] = u.customConfig.Manufacturer + u.configMap["base_info"].attrs["product"] = u.customConfig.Product +} + +func (u *UsbGadget) SetGadgetConfig(config *Config) { + u.configLock.Lock() + defer u.configLock.Unlock() + + if config == nil { + return // nothing to do + } + + u.customConfig = *config + u.loadGadgetConfig() +} + +func (u *UsbGadget) SetGadgetDevices(devices *Devices) { + u.configLock.Lock() + defer u.configLock.Unlock() + + if devices == nil { + return // nothing to do + } + + u.enabledDevices = *devices +} + +// GetConfigPath returns the path to the config item. +func (u *UsbGadget) GetConfigPath(itemKey string) (string, error) { + item, ok := u.configMap[itemKey] + if !ok { + return "", fmt.Errorf("config item %s not found", itemKey) + } + return joinPath(u.kvmGadgetPath, item.configPath), nil +} + +// GetPath returns the path to the item. +func (u *UsbGadget) GetPath(itemKey string) (string, error) { + item, ok := u.configMap[itemKey] + if !ok { + return "", fmt.Errorf("config item %s not found", itemKey) + } + return joinPath(u.kvmGadgetPath, item.path), nil +} + +func mountConfigFS() error { + _, err := os.Stat(gadgetPath) + // TODO: check if it's mounted properly + if err == nil { + return nil + } + + if os.IsNotExist(err) { + err = exec.Command("mount", "-t", "configfs", "none", configFSPath).Run() + if err != nil { + return fmt.Errorf("failed to mount configfs: %w", err) + } + } else { + return fmt.Errorf("unable to access usb gadget path: %w", err) + } + return nil +} + +func (u *UsbGadget) Init() error { + u.configLock.Lock() + defer u.configLock.Unlock() + + u.loadGadgetConfig() + + udcs := getUdcs() + if len(udcs) < 1 { + u.log.Error("no udc found, skipping USB stack init") + return nil + } + + u.udc = udcs[0] + _, err := os.Stat(u.kvmGadgetPath) + if err == nil { + u.log.Info("usb gadget already exists") + } + + if err := mountConfigFS(); err != nil { + u.log.Errorf("failed to mount configfs: %v, usb stack might not function properly", err) + } + + if err := os.MkdirAll(u.configC1Path, 0755); err != nil { + u.log.Errorf("failed to create config path: %v", err) + } + + if err := u.writeGadgetConfig(); err != nil { + u.log.Errorf("failed to start gadget: %v", err) + } + + return nil +} + +func (u *UsbGadget) UpdateGadgetConfig() error { + u.configLock.Lock() + defer u.configLock.Unlock() + + u.loadGadgetConfig() + + if err := u.writeGadgetConfig(); err != nil { + u.log.Errorf("failed to update gadget: %v", err) + } + + return nil +} + +func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems { + items := make([]gadgetConfigItemWithKey, 0) + for key, item := range u.configMap { + items = append(items, gadgetConfigItemWithKey{key, item}) + } + + sort.Slice(items, func(i, j int) bool { + return items[i].item.order < items[j].item.order + }) + + return items +} + +func (u *UsbGadget) writeGadgetConfig() error { + // create kvm gadget path + err := os.MkdirAll(u.kvmGadgetPath, 0755) + if err != nil { + return err + } + + u.log.Tracef("writing gadget config") + for _, val := range u.getOrderedConfigItems() { + key := val.key + item := val.item + + // check if the item is enabled in the config + if !u.isGadgetConfigItemEnabled(key) { + u.log.Tracef("disabling gadget config: %s", key) + err = u.disableGadgetItemConfig(item) + if err != nil { + return err + } + continue + } + u.log.Tracef("writing gadget config: %s", key) + err = u.writeGadgetItemConfig(item) + if err != nil { + return err + } + } + + if err = u.writeUDC(); err != nil { + u.log.Errorf("failed to write UDC: %v", err) + return err + } + + if err = u.rebindUsb(true); err != nil { + u.log.Infof("failed to rebind usb: %v", err) + } + + return nil +} + +func (u *UsbGadget) disableGadgetItemConfig(item gadgetConfigItem) error { + // remove symlink if exists + if item.configPath == nil { + return nil + } + + configPath := joinPath(u.configC1Path, item.configPath) + + if _, err := os.Lstat(configPath); os.IsNotExist(err) { + u.log.Tracef("symlink %s does not exist", item.configPath) + return nil + } + + if err := os.Remove(configPath); err != nil { + return fmt.Errorf("failed to remove symlink %s: %w", item.configPath, err) + } + + return nil +} + +func (u *UsbGadget) writeGadgetItemConfig(item gadgetConfigItem) error { + // create directory for the item + gadgetItemPath := joinPath(u.kvmGadgetPath, item.path) + err := os.MkdirAll(gadgetItemPath, 0755) + if err != nil { + return fmt.Errorf("failed to create path %s: %w", gadgetItemPath, err) + } + + if len(item.attrs) > 0 { + // write attributes for the item + err = u.writeGadgetAttrs(gadgetItemPath, item.attrs) + if err != nil { + return fmt.Errorf("failed to write attributes for %s: %w", gadgetItemPath, err) + } + } + + // write report descriptor if available + if item.reportDesc != nil { + err = u.writeIfDifferent(path.Join(gadgetItemPath, "report_desc"), item.reportDesc, 0644) + if err != nil { + return err + } + } + + // create config directory if configAttrs are set + if len(item.configAttrs) > 0 { + configItemPath := joinPath(u.configC1Path, item.configPath) + err = os.MkdirAll(configItemPath, 0755) + if err != nil { + return fmt.Errorf("failed to create path %s: %w", configItemPath, err) + } + + err = u.writeGadgetAttrs(configItemPath, item.configAttrs) + if err != nil { + return fmt.Errorf("failed to write config attributes for %s: %w", configItemPath, err) + } + } + + // create symlink if configPath is set + if item.configPath != nil && item.configAttrs == nil { + configPath := joinPath(u.configC1Path, item.configPath) + u.log.Tracef("Creating symlink from %s to %s", configPath, gadgetItemPath) + if err := ensureSymlink(configPath, gadgetItemPath); err != nil { + return err + } + } + + return nil +} + +func (u *UsbGadget) writeGadgetAttrs(basePath string, attrs gadgetAttributes) error { + for key, val := range attrs { + filePath := filepath.Join(basePath, key) + err := u.writeIfDifferent(filePath, []byte(val), 0644) + if err != nil { + return fmt.Errorf("failed to write to %s: %w", filePath, err) + } + } + return nil +} diff --git a/internal/usbgadget/consts.go b/internal/usbgadget/consts.go new file mode 100644 index 0000000..8204d0a --- /dev/null +++ b/internal/usbgadget/consts.go @@ -0,0 +1,3 @@ +package usbgadget + +const dwc3Path = "/sys/bus/platform/drivers/dwc3" diff --git a/internal/usbgadget/hid.go b/internal/usbgadget/hid.go new file mode 100644 index 0000000..5faac89 --- /dev/null +++ b/internal/usbgadget/hid.go @@ -0,0 +1,11 @@ +package usbgadget + +import "time" + +func (u *UsbGadget) resetUserInputTime() { + u.lastUserInput = time.Now() +} + +func (u *UsbGadget) GetLastUserInputTime() time.Time { + return u.lastUserInput +} diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go new file mode 100644 index 0000000..030f7af --- /dev/null +++ b/internal/usbgadget/hid_keyboard.go @@ -0,0 +1,95 @@ +package usbgadget + +import ( + "fmt" + "os" +) + +var keyboardConfig = gadgetConfigItem{ + order: 1000, + device: "hid.usb0", + path: []string{"functions", "hid.usb0"}, + configPath: []string{"hid.usb0"}, + attrs: gadgetAttributes{ + "protocol": "1", + "subclass": "1", + "report_length": "8", + }, + reportDesc: keyboardReportDesc, +} + +// Source: https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt +var keyboardReportDesc = []byte{ + 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ + 0x09, 0x06, /* USAGE (Keyboard) */ + 0xa1, 0x01, /* COLLECTION (Application) */ + 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ + 0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */ + 0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */ + 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ + 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x95, 0x08, /* REPORT_COUNT (8) */ + 0x81, 0x02, /* INPUT (Data,Var,Abs) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x08, /* REPORT_SIZE (8) */ + 0x81, 0x03, /* INPUT (Cnst,Var,Abs) */ + 0x95, 0x05, /* REPORT_COUNT (5) */ + 0x75, 0x01, /* REPORT_SIZE (1) */ + 0x05, 0x08, /* USAGE_PAGE (LEDs) */ + 0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */ + 0x29, 0x05, /* USAGE_MAXIMUM (Kana) */ + 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ + 0x95, 0x01, /* REPORT_COUNT (1) */ + 0x75, 0x03, /* REPORT_SIZE (3) */ + 0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */ + 0x95, 0x06, /* REPORT_COUNT (6) */ + 0x75, 0x08, /* REPORT_SIZE (8) */ + 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ + 0x25, 0x65, /* LOGICAL_MAXIMUM (101) */ + 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ + 0x19, 0x00, /* USAGE_MINIMUM (Reserved) */ + 0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */ + 0x81, 0x00, /* INPUT (Data,Ary,Abs) */ + 0xc0, /* END_COLLECTION */ +} + +func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { + if u.keyboardHidFile == nil { + var err error + u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666) + if err != nil { + return fmt.Errorf("failed to open hidg0: %w", err) + } + } + + _, err := u.keyboardHidFile.Write(data) + if err != nil { + u.log.Errorf("failed to write to hidg0: %w", err) + u.keyboardHidFile.Close() + u.keyboardHidFile = nil + return err + } + + return nil +} + +func (u *UsbGadget) KeyboardReport(modifier uint8, keys []uint8) error { + u.keyboardLock.Lock() + defer u.keyboardLock.Unlock() + + if len(keys) > 6 { + keys = keys[:6] + } + if len(keys) < 6 { + keys = append(keys, make([]uint8, 6-len(keys))...) + } + + err := u.keyboardWriteHidFile([]byte{modifier, 0, keys[0], keys[1], keys[2], keys[3], keys[4], keys[5]}) + if err != nil { + return err + } + + u.resetUserInputTime() + return nil +} diff --git a/internal/usbgadget/hid_mouse_absolute.go b/internal/usbgadget/hid_mouse_absolute.go new file mode 100644 index 0000000..c59b591 --- /dev/null +++ b/internal/usbgadget/hid_mouse_absolute.go @@ -0,0 +1,128 @@ +package usbgadget + +import ( + "fmt" + "os" +) + +var absoluteMouseConfig = gadgetConfigItem{ + order: 1001, + device: "hid.usb1", + path: []string{"functions", "hid.usb1"}, + configPath: []string{"hid.usb1"}, + attrs: gadgetAttributes{ + "protocol": "2", + "subclass": "1", + "report_length": "6", + }, + reportDesc: absoluteMouseCombinedReportDesc, +} + +var absoluteMouseCombinedReportDesc = []byte{ + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x09, 0x02, // Usage (Mouse) + 0xA1, 0x01, // Collection (Application) + + // Report ID 1: Absolute Mouse Movement + 0x85, 0x01, // Report ID (1) + 0x09, 0x01, // Usage (Pointer) + 0xA1, 0x00, // Collection (Physical) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Minimum (0x01) + 0x29, 0x03, // Usage Maximum (0x03) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x01, // Report Size (1) + 0x95, 0x03, // Report Count (3) + 0x81, 0x02, // Input (Data, Var, Abs) + 0x95, 0x01, // Report Count (1) + 0x75, 0x05, // Report Size (5) + 0x81, 0x03, // Input (Cnst, Var, Abs) + 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x16, 0x00, 0x00, // Logical Minimum (0) + 0x26, 0xFF, 0x7F, // Logical Maximum (32767) + 0x36, 0x00, 0x00, // Physical Minimum (0) + 0x46, 0xFF, 0x7F, // Physical Maximum (32767) + 0x75, 0x10, // Report Size (16) + 0x95, 0x02, // Report Count (2) + 0x81, 0x02, // Input (Data, Var, Abs) + 0xC0, // End Collection + + // Report ID 2: Relative Wheel Movement + 0x85, 0x02, // Report ID (2) + 0x09, 0x38, // Usage (Wheel) + 0x15, 0x81, // Logical Minimum (-127) + 0x25, 0x7F, // Logical Maximum (127) + 0x75, 0x08, // Report Size (8) + 0x95, 0x01, // Report Count (1) + 0x81, 0x06, // Input (Data, Var, Rel) + + 0xC0, // End Collection +} + +func (u *UsbGadget) absMouseWriteHidFile(data []byte) error { + if u.absMouseHidFile == nil { + var err error + u.absMouseHidFile, err = os.OpenFile("/dev/hidg1", os.O_RDWR, 0666) + if err != nil { + return fmt.Errorf("failed to open hidg1: %w", err) + } + } + + _, err := u.absMouseHidFile.Write(data) + if err != nil { + u.log.Errorf("failed to write to hidg1: %w", err) + u.absMouseHidFile.Close() + u.absMouseHidFile = nil + return err + } + return nil +} + +func (u *UsbGadget) AbsMouseReport(x, y int, buttons uint8) error { + u.absMouseLock.Lock() + defer u.absMouseLock.Unlock() + + err := u.absMouseWriteHidFile([]byte{ + 1, // Report ID 1 + buttons, // Buttons + uint8(x), // X Low Byte + uint8(x >> 8), // X High Byte + uint8(y), // Y Low Byte + uint8(y >> 8), // Y High Byte + }) + if err != nil { + return err + } + + u.resetUserInputTime() + return nil +} + +func (u *UsbGadget) AbsMouseWheelReport(wheelY int8) error { + u.absMouseLock.Lock() + defer u.absMouseLock.Unlock() + + // Accumulate the wheelY value + u.absMouseAccumulatedWheelY += float64(wheelY) / 8.0 + + // Only send a report if the accumulated value is significant + if abs(u.absMouseAccumulatedWheelY) < 1.0 { + return nil + } + + scaledWheelY := int8(u.absMouseAccumulatedWheelY) + + err := u.absMouseWriteHidFile([]byte{ + 2, // Report ID 2 + byte(scaledWheelY), // Scaled Wheel Y (signed) + }) + + // Reset the accumulator, keeping any remainder + u.absMouseAccumulatedWheelY -= float64(scaledWheelY) + + u.resetUserInputTime() + return err +} diff --git a/internal/usbgadget/hid_mouse_relative.go b/internal/usbgadget/hid_mouse_relative.go new file mode 100644 index 0000000..df844dc --- /dev/null +++ b/internal/usbgadget/hid_mouse_relative.go @@ -0,0 +1,92 @@ +package usbgadget + +import ( + "fmt" + "os" +) + +var relativeMouseConfig = gadgetConfigItem{ + order: 1002, + device: "hid.usb2", + path: []string{"functions", "hid.usb2"}, + configPath: []string{"hid.usb2"}, + attrs: gadgetAttributes{ + "protocol": "2", + "subclass": "1", + "report_length": "4", + }, + reportDesc: relativeMouseCombinedReportDesc, +} + +// from: https://github.com/NicoHood/HID/blob/b16be57caef4295c6cd382a7e4c64db5073647f7/src/SingleReport/BootMouse.cpp#L26 +var relativeMouseCombinedReportDesc = []byte{ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 54 + 0x09, 0x02, // USAGE (Mouse) + 0xa1, 0x01, // COLLECTION (Application) + + // Pointer and Physical are required by Apple Recovery + 0x09, 0x01, // USAGE (Pointer) + 0xa1, 0x00, // COLLECTION (Physical) + + // 8 Buttons + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM (Button 1) + 0x29, 0x08, // USAGE_MAXIMUM (Button 8) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x95, 0x08, // REPORT_COUNT (8) + 0x75, 0x01, // REPORT_SIZE (1) + 0x81, 0x02, // INPUT (Data,Var,Abs) + + // X, Y, Wheel + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x09, 0x38, // USAGE (Wheel) + 0x15, 0x81, // LOGICAL_MINIMUM (-127) + 0x25, 0x7f, // LOGICAL_MAXIMUM (127) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x03, // REPORT_COUNT (3) + 0x81, 0x06, // INPUT (Data,Var,Rel) + + // End + 0xc0, // End Collection (Physical) + 0xc0, // End Collection +} + +func (u *UsbGadget) relMouseWriteHidFile(data []byte) error { + if u.relMouseHidFile == nil { + var err error + u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666) + if err != nil { + return fmt.Errorf("failed to open hidg1: %w", err) + } + } + + _, err := u.relMouseHidFile.Write(data) + if err != nil { + u.log.Errorf("failed to write to hidg2: %w", err) + u.relMouseHidFile.Close() + u.relMouseHidFile = nil + return err + } + return nil +} + +func (u *UsbGadget) RelMouseReport(mx, my int8, buttons uint8) error { + u.relMouseLock.Lock() + defer u.relMouseLock.Unlock() + + err := u.relMouseWriteHidFile([]byte{ + buttons, // Buttons + uint8(mx), // X + uint8(my), // Y + 0, // Wheel + }) + if err != nil { + return err + } + + u.resetUserInputTime() + return nil +} diff --git a/internal/usbgadget/mass_storage.go b/internal/usbgadget/mass_storage.go new file mode 100644 index 0000000..f962cb4 --- /dev/null +++ b/internal/usbgadget/mass_storage.go @@ -0,0 +1,23 @@ +package usbgadget + +var massStorageBaseConfig = gadgetConfigItem{ + order: 3000, + device: "mass_storage.usb0", + path: []string{"functions", "mass_storage.usb0"}, + configPath: []string{"mass_storage.usb0"}, + attrs: gadgetAttributes{ + "stall": "1", + }, +} + +var massStorageLun0Config = gadgetConfigItem{ + order: 3001, + path: []string{"functions", "mass_storage.usb0", "lun.0"}, + attrs: gadgetAttributes{ + "cdrom": "1", + "ro": "1", + "removable": "1", + "file": "\n", + "inquiry_string": "JetKVM Virtual Media", + }, +} diff --git a/internal/usbgadget/udc.go b/internal/usbgadget/udc.go new file mode 100644 index 0000000..6316b83 --- /dev/null +++ b/internal/usbgadget/udc.go @@ -0,0 +1,109 @@ +package usbgadget + +import ( + "fmt" + "os" + "path" + "strings" +) + +func getUdcs() []string { + var udcs []string + + files, err := os.ReadDir("/sys/devices/platform/usbdrd") + if err != nil { + return nil + } + + for _, file := range files { + if !file.IsDir() || !strings.HasSuffix(file.Name(), ".usb") { + continue + } + udcs = append(udcs, file.Name()) + } + + return udcs +} + +func rebindUsb(udc string, ignoreUnbindError bool) error { + err := os.WriteFile(path.Join(dwc3Path, "unbind"), []byte(udc), 0644) + if err != nil && !ignoreUnbindError { + return err + } + err = os.WriteFile(path.Join(dwc3Path, "bind"), []byte(udc), 0644) + if err != nil { + return err + } + return nil +} + +func (u *UsbGadget) rebindUsb(ignoreUnbindError bool) error { + u.log.Infof("rebinding USB gadget to UDC %s", u.udc) + return rebindUsb(u.udc, ignoreUnbindError) +} + +// RebindUsb rebinds the USB gadget to the UDC. +func (u *UsbGadget) RebindUsb(ignoreUnbindError bool) error { + u.configLock.Lock() + defer u.configLock.Unlock() + + return u.rebindUsb(ignoreUnbindError) +} + +func (u *UsbGadget) writeUDC() error { + path := path.Join(u.kvmGadgetPath, "UDC") + + u.log.Tracef("writing UDC %s to %s", u.udc, path) + err := u.writeIfDifferent(path, []byte(u.udc), 0644) + if err != nil { + return fmt.Errorf("failed to write UDC: %w", err) + } + + return nil +} + +// GetUsbState returns the current state of the USB gadget +func (u *UsbGadget) GetUsbState() (state string) { + stateFile := path.Join("/sys/class/udc", u.udc, "state") + stateBytes, err := os.ReadFile(stateFile) + if err != nil { + if os.IsNotExist(err) { + return "not attached" + } else { + u.log.Tracef("failed to read usb state: %v", err) + } + return "unknown" + } + return strings.TrimSpace(string(stateBytes)) +} + +// IsUDCBound checks if the UDC state is bound. +func (u *UsbGadget) IsUDCBound() (bool, error) { + udcFilePath := path.Join(dwc3Path, u.udc) + _, err := os.Stat(udcFilePath) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, fmt.Errorf("error checking USB emulation state: %w", err) + } + return true, nil +} + +// BindUDC binds the gadget to the UDC. +func (u *UsbGadget) BindUDC() error { + err := os.WriteFile(path.Join(dwc3Path, "bind"), []byte(u.udc), 0644) + if err != nil { + return fmt.Errorf("error binding UDC: %w", err) + } + return nil +} + +// UnbindUDC unbinds the gadget from the UDC. +func (u *UsbGadget) UnbindUDC() error { + err := os.WriteFile(path.Join(dwc3Path, "unbind"), []byte(u.udc), 0644) + if err != nil { + return fmt.Errorf("error unbinding UDC: %w", err) + } + return nil +} diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go new file mode 100644 index 0000000..9fc34d5 --- /dev/null +++ b/internal/usbgadget/usbgadget.go @@ -0,0 +1,110 @@ +// Package usbgadget provides a high-level interface to manage USB gadgets +// THIS PACKAGE IS FOR INTERNAL USE ONLY AND ITS API MAY CHANGE WITHOUT NOTICE +package usbgadget + +import ( + "os" + "path" + "sync" + "time" + + "github.com/pion/logging" +) + +// Devices is a struct that represents the USB devices that can be enabled on a USB gadget. +type Devices struct { + AbsoluteMouse bool `json:"absolute_mouse"` + RelativeMouse bool `json:"relative_mouse"` + Keyboard bool `json:"keyboard"` + MassStorage bool `json:"mass_storage"` +} + +// Config is a struct that represents the customizations for a USB gadget. +// TODO: rename to something else that won't confuse with the USB gadget configuration +type Config struct { + VendorId string `json:"vendor_id"` + ProductId string `json:"product_id"` + SerialNumber string `json:"serial_number"` + Manufacturer string `json:"manufacturer"` + Product string `json:"product"` + + isEmpty bool +} + +var defaultUsbGadgetDevices = Devices{ + AbsoluteMouse: true, + RelativeMouse: true, + Keyboard: true, + MassStorage: true, +} + +// UsbGadget is a struct that represents a USB gadget. +type UsbGadget struct { + name string + udc string + kvmGadgetPath string + configC1Path string + + configMap map[string]gadgetConfigItem + customConfig Config + + configLock sync.Mutex + + keyboardHidFile *os.File + keyboardLock sync.Mutex + absMouseHidFile *os.File + absMouseLock sync.Mutex + relMouseHidFile *os.File + relMouseLock sync.Mutex + + enabledDevices Devices + + absMouseAccumulatedWheelY float64 + + lastUserInput time.Time + + log logging.LeveledLogger +} + +const configFSPath = "/sys/kernel/config" +const gadgetPath = "/sys/kernel/config/usb_gadget" + +var defaultLogger = logging.NewDefaultLoggerFactory().NewLogger("usbgadget") + +// NewUsbGadget creates a new UsbGadget. +func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *logging.LeveledLogger) *UsbGadget { + if logger == nil { + logger = &defaultLogger + } + + if enabledDevices == nil { + enabledDevices = &defaultUsbGadgetDevices + } + + if config == nil { + config = &Config{isEmpty: true} + } + + g := &UsbGadget{ + name: name, + kvmGadgetPath: path.Join(gadgetPath, name), + configC1Path: path.Join(gadgetPath, name, "configs/c.1"), + configMap: defaultGadgetConfig, + customConfig: *config, + configLock: sync.Mutex{}, + keyboardLock: sync.Mutex{}, + absMouseLock: sync.Mutex{}, + relMouseLock: sync.Mutex{}, + enabledDevices: *enabledDevices, + lastUserInput: time.Now(), + log: *logger, + + absMouseAccumulatedWheelY: 0, + } + if err := g.Init(); err != nil { + g.log.Errorf("failed to init USB gadget: %v", err) + return nil + } + + return g +} diff --git a/internal/usbgadget/utils.go b/internal/usbgadget/utils.go new file mode 100644 index 0000000..3f0adda --- /dev/null +++ b/internal/usbgadget/utils.go @@ -0,0 +1,63 @@ +package usbgadget + +import ( + "bytes" + "fmt" + "os" + "path/filepath" +) + +// Helper function to get absolute value of float64 +func abs(x float64) float64 { + if x < 0 { + return -x + } + return x +} + +func joinPath(basePath string, paths []string) string { + pathArr := append([]string{basePath}, paths...) + return filepath.Join(pathArr...) +} + +func ensureSymlink(linkPath string, target string) error { + if _, err := os.Lstat(linkPath); err == nil { + currentTarget, err := os.Readlink(linkPath) + if err != nil || currentTarget != target { + err = os.Remove(linkPath) + if err != nil { + return fmt.Errorf("failed to remove existing symlink %s: %w", linkPath, err) + } + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("failed to check if symlink exists: %w", err) + } + + if err := os.Symlink(target, linkPath); err != nil { + return fmt.Errorf("failed to create symlink from %s to %s: %w", linkPath, target, err) + } + + return nil +} + +func (u *UsbGadget) writeIfDifferent(filePath string, content []byte, permMode os.FileMode) error { + if _, err := os.Stat(filePath); err == nil { + oldContent, err := os.ReadFile(filePath) + if err == nil { + if bytes.Equal(oldContent, content) { + u.log.Tracef("skipping writing to %s as it already has the correct content", filePath) + return nil + } + + if len(oldContent) == len(content)+1 && + bytes.Equal(oldContent[:len(content)], content) && + oldContent[len(content)] == 10 { + u.log.Tracef("skipping writing to %s as it already has the correct content", filePath) + return nil + } + + u.log.Tracef("writing to %s as it has different content old%v new%v", filePath, oldContent, content) + } + } + return os.WriteFile(filePath, content, permMode) +} diff --git a/jiggler.go b/jiggler.go index 06f2b6c..daec192 100644 --- a/jiggler.go +++ b/jiggler.go @@ -6,10 +6,6 @@ import ( var lastUserInput = time.Now() -func resetUserInputTime() { - lastUserInput = time.Now() -} - var jigglerEnabled = false func rpcSetJigglerState(enabled bool) { @@ -20,6 +16,8 @@ func rpcGetJigglerState() bool { } func init() { + ensureConfigLoaded() + go runJiggler() } diff --git a/jsonrpc.go b/jsonrpc.go index 619e561..64935e1 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "log" "os" "os/exec" "path/filepath" @@ -15,6 +14,8 @@ import ( "github.com/pion/webrtc/v4" "go.bug.st/serial" + + "github.com/jetkvm/kvm/internal/usbgadget" ) type JSONRPCRequest struct { @@ -46,12 +47,12 @@ type BacklightSettings struct { func writeJSONRPCResponse(response JSONRPCResponse, session *Session) { responseBytes, err := json.Marshal(response) if err != nil { - log.Println("Error marshalling JSONRPC response:", err) + logger.Warnf("Error marshalling JSONRPC response: %v", err) return } err = session.RPCChannel.SendText(string(responseBytes)) if err != nil { - log.Println("Error sending JSONRPC response:", err) + logger.Warnf("Error sending JSONRPC response: %v", err) return } } @@ -64,16 +65,16 @@ func writeJSONRPCEvent(event string, params interface{}, session *Session) { } requestBytes, err := json.Marshal(request) if err != nil { - log.Println("Error marshalling JSONRPC event:", err) + logger.Warnf("Error marshalling JSONRPC event: %v", err) return } if session == nil || session.RPCChannel == nil { - log.Println("RPC channel not available") + logger.Info("RPC channel not available") return } err = session.RPCChannel.SendText(string(requestBytes)) if err != nil { - log.Println("Error sending JSONRPC event:", err) + logger.Warnf("Error sending JSONRPC event: %v", err) return } } @@ -94,7 +95,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { return } - //log.Printf("Received RPC request: Method=%s, Params=%v, ID=%d", request.Method, request.Params, request.ID) + //logger.Infof("Received RPC request: Method=%s, Params=%v, ID=%d", request.Method, request.Params, request.ID) handler, ok := rpcHandlers[request.Method] if !ok { errorResponse := JSONRPCResponse{ @@ -147,7 +148,7 @@ func rpcGetStreamQualityFactor() (float64, error) { } func rpcSetStreamQualityFactor(factor float64) error { - log.Printf("Setting stream quality factor to: %f", factor) + logger.Infof("Setting stream quality factor to: %f", factor) var _, err = CallCtrlAction("set_video_quality_factor", map[string]interface{}{"quality_factor": factor}) if err != nil { return err @@ -183,10 +184,10 @@ func rpcGetEDID() (string, error) { func rpcSetEDID(edid string) error { if edid == "" { - log.Println("Restoring EDID to default") + logger.Info("Restoring EDID to default") edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b" } else { - log.Printf("Setting EDID to: %s", edid) + logger.Infof("Setting EDID to: %s", edid) } _, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid}) if err != nil { @@ -195,8 +196,7 @@ func rpcSetEDID(edid string) error { // Save EDID to config, allowing it to be restored on reboot. config.EdidString = edid - SaveConfig() - + _ = SaveConfig() return nil } @@ -257,7 +257,7 @@ func rpcSetBacklightSettings(params BacklightSettings) error { return fmt.Errorf("failed to save config: %w", err) } - log.Printf("rpc: display: settings applied, max_brightness: %d, dim after: %ds, off after: %ds", config.DisplayMaxBrightness, config.DisplayDimAfterSec, config.DisplayOffAfterSec) + logger.Infof("rpc: display: settings applied, max_brightness: %d, dim after: %ds, off after: %ds", config.DisplayMaxBrightness, config.DisplayDimAfterSec, config.DisplayOffAfterSec) // If the device started up with auto-dim and/or auto-off set to zero, the display init // method will not have started the tickers. So in case that has changed, attempt to start the tickers now. @@ -478,23 +478,23 @@ type RPCHandler struct { } func rpcSetMassStorageMode(mode string) (string, error) { - log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) + logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) var cdrom bool if mode == "cdrom" { cdrom = true } else if mode != "file" { - log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Invalid mode provided: %s", mode) + logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Invalid mode provided: %s", mode) return "", fmt.Errorf("invalid mode: %s", mode) } - log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) + logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) err := setMassStorageMode(cdrom) if err != nil { return "", fmt.Errorf("failed to set mass storage mode: %w", err) } - log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Mass storage mode set to %s", mode) + logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Mass storage mode set to %s", mode) // Get the updated mode after setting return rpcGetMassStorageMode() @@ -517,27 +517,30 @@ func rpcIsUpdatePending() (bool, error) { return IsUpdatePending(), nil } -var udcFilePath = filepath.Join("/sys/bus/platform/drivers/dwc3", udc) - func rpcGetUsbEmulationState() (bool, error) { - _, err := os.Stat(udcFilePath) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, fmt.Errorf("error checking USB emulation state: %w", err) - } - return true, nil + return gadget.IsUDCBound() } func rpcSetUsbEmulationState(enabled bool) error { if enabled { - return os.WriteFile("/sys/bus/platform/drivers/dwc3/bind", []byte(udc), 0644) + return gadget.BindUDC() } else { - return os.WriteFile("/sys/bus/platform/drivers/dwc3/unbind", []byte(udc), 0644) + return gadget.UnbindUDC() } } +func rpcGetUsbConfig() (usbgadget.Config, error) { + LoadConfig() + return *config.UsbConfig, nil +} + +func rpcSetUsbConfig(usbConfig usbgadget.Config) error { + LoadConfig() + config.UsbConfig = &usbConfig + gadget.SetGadgetConfig(config.UsbConfig) + return updateUsbRelatedConfig() +} + func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { if config.WakeOnLanDevices == nil { return []WakeOnLanDevice{}, nil @@ -560,7 +563,7 @@ func rpcResetConfig() error { return fmt.Errorf("failed to reset config: %w", err) } - log.Println("Configuration reset to default") + logger.Info("Configuration reset to default") return nil } @@ -576,7 +579,7 @@ func rpcGetDCPowerState() (DCPowerState, error) { } func rpcSetDCPowerState(enabled bool) error { - log.Printf("[jsonrpc.go:rpcSetDCPowerState] Setting DC power state to: %v", enabled) + logger.Infof("[jsonrpc.go:rpcSetDCPowerState] Setting DC power state to: %v", enabled) err := setDCPowerState(enabled) if err != nil { return fmt.Errorf("failed to set DC power state: %w", err) @@ -593,18 +596,18 @@ func rpcSetActiveExtension(extensionId string) error { return nil } if config.ActiveExtension == "atx-power" { - unmountATXControl() + _ = unmountATXControl() } else if config.ActiveExtension == "dc-power" { - unmountDCControl() + _ = unmountDCControl() } config.ActiveExtension = extensionId if err := SaveConfig(); err != nil { return fmt.Errorf("failed to save config: %w", err) } if extensionId == "atx-power" { - mountATXControl() + _ = mountATXControl() } else if extensionId == "dc-power" { - mountDCControl() + _ = mountDCControl() } return nil } @@ -725,11 +728,70 @@ func rpcSetSerialSettings(settings SerialSettings) error { Parity: parity, } - port.SetMode(serialPortMode) + _ = port.SetMode(serialPortMode) return nil } +func rpcGetUsbDevices() (usbgadget.Devices, error) { + return *config.UsbDevices, nil +} + +func updateUsbRelatedConfig() error { + if err := gadget.UpdateGadgetConfig(); err != nil { + return fmt.Errorf("failed to write gadget config: %w", err) + } + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + return nil +} + +func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { + config.UsbDevices = &usbDevices + gadget.SetGadgetDevices(config.UsbDevices) + return updateUsbRelatedConfig() +} + +func rpcSetUsbDeviceState(device string, enabled bool) error { + switch device { + case "absoluteMouse": + config.UsbDevices.AbsoluteMouse = enabled + case "relativeMouse": + config.UsbDevices.RelativeMouse = enabled + case "keyboard": + config.UsbDevices.Keyboard = enabled + case "massStorage": + config.UsbDevices.MassStorage = enabled + default: + return fmt.Errorf("invalid device: %s", device) + } + gadget.SetGadgetDevices(config.UsbDevices) + return updateUsbRelatedConfig() +} + +func rpcSetCloudUrl(apiUrl string, appUrl string) error { + config.CloudURL = apiUrl + config.CloudAppURL = appUrl + + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + return nil +} + +var currentScrollSensitivity string = "default" + +func rpcGetScrollSensitivity() (string, error) { + return currentScrollSensitivity, nil +} + +func rpcSetScrollSensitivity(sensitivity string) error { + currentScrollSensitivity = sensitivity + return nil +} + var rpcHandlers = map[string]RPCHandler{ "ping": {Func: rpcPing}, "getDeviceID": {Func: rpcGetDeviceID}, @@ -737,6 +799,7 @@ var rpcHandlers = map[string]RPCHandler{ "getCloudState": {Func: rpcGetCloudState}, "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, "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}, @@ -764,6 +827,8 @@ var rpcHandlers = map[string]RPCHandler{ "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}, @@ -786,4 +851,10 @@ var rpcHandlers = map[string]RPCHandler{ "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"}}, + "getScrollSensitivity": {Func: rpcGetScrollSensitivity}, + "setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}}, } diff --git a/log.go b/log.go index 89ad1d2..7718a28 100644 --- a/log.go +++ b/log.go @@ -5,4 +5,4 @@ import "github.com/pion/logging" // we use logging framework from pion // ref: https://github.com/pion/webrtc/wiki/Debugging-WebRTC var logger = logging.NewDefaultLoggerFactory().NewLogger("jetkvm") -var usbLogger = logging.NewDefaultLoggerFactory().NewLogger("usb") +var cloudLogger = logging.NewDefaultLoggerFactory().NewLogger("cloud") diff --git a/main.go b/main.go index e23e9c8..6a55595 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package kvm import ( "context" - "log" "net/http" "os" "os/signal" @@ -36,6 +35,8 @@ func Main() { StartNativeCtrlSocketServer() StartNativeVideoSocketServer() + initPrometheus() + go func() { err = ExtractAndRunNativeBin() if err != nil { @@ -44,11 +45,13 @@ func Main() { } }() + initUsbGadget() + go func() { time.Sleep(15 * time.Minute) for { logger.Debugf("UPDATING - Auto update enabled: %v", config.AutoUpdateEnabled) - if config.AutoUpdateEnabled == false { + if !config.AutoUpdateEnabled { return } if currentSession != nil { @@ -66,6 +69,9 @@ func Main() { }() //go RunFuseServer() go RunWebServer() + if config.TLSMode != "" { + go RunWebSecureServer() + } // If the cloud token isn't set, the client won't be started by default. // However, if the user adopts the device via the web interface, handleCloudRegister will start the client. if config.CloudToken != "" { @@ -75,15 +81,15 @@ func Main() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs - log.Println("JetKVM Shutting Down") + logger.Info("JetKVM Shutting Down") //if fuseServer != nil { // err := setMassStorageImage(" ") // if err != nil { - // log.Printf("Failed to unmount mass storage image: %v", err) + // logger.Infof("Failed to unmount mass storage image: %v", err) // } // err = fuseServer.Unmount() // if err != nil { - // log.Printf("Failed to unmount fuse: %v", err) + // logger.Infof("Failed to unmount fuse: %v", err) // } // os.Exit(0) diff --git a/native.go b/native.go index 7940f64..8960304 100644 --- a/native.go +++ b/native.go @@ -5,8 +5,6 @@ import ( "encoding/json" "fmt" "io" - "kvm/resource" - "log" "net" "os" "os/exec" @@ -14,6 +12,8 @@ import ( "syscall" "time" + "github.com/jetkvm/kvm/resource" + "github.com/pion/webrtc/v4/pkg/media" ) @@ -61,7 +61,7 @@ func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse return nil, fmt.Errorf("error marshaling ctrl action: %w", err) } - fmt.Println("sending ctrl action", string(jsonData)) + logger.Infof("sending ctrl action: %s", string(jsonData)) err = WriteCtrlMessage(jsonData) if err != nil { @@ -91,8 +91,8 @@ func WriteCtrlMessage(message []byte) error { return err } -var nativeCtrlSocketListener net.Listener -var nativeVideoSocketListener net.Listener +var nativeCtrlSocketListener net.Listener //nolint:unused +var nativeVideoSocketListener net.Listener //nolint:unused var ctrlClientConnected = make(chan struct{}) @@ -104,16 +104,18 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC // Remove the socket file if it already exists if _, err := os.Stat(socketPath); err == nil { if err := os.Remove(socketPath); err != nil { - log.Fatalf("Failed to remove existing socket file %s: %v", socketPath, err) + logger.Errorf("Failed to remove existing socket file %s: %v", socketPath, err) + os.Exit(1) } } listener, err := net.Listen("unixpacket", socketPath) if err != nil { - log.Fatalf("Failed to start server on %s: %v", socketPath, err) + logger.Errorf("Failed to start server on %s: %v", socketPath, err) + os.Exit(1) } - log.Printf("Server listening on %s", socketPath) + logger.Infof("Server listening on %s", socketPath) go func() { conn, err := listener.Accept() @@ -188,24 +190,23 @@ func handleCtrlClient(conn net.Conn) { func handleVideoClient(conn net.Conn) { defer conn.Close() - log.Printf("Native video socket client connected: %v", conn.RemoteAddr()) + logger.Infof("Native video socket client connected: %v", conn.RemoteAddr()) inboundPacket := make([]byte, maxFrameSize) lastFrame := time.Now() for { n, err := conn.Read(inboundPacket) if err != nil { - log.Println("error during read: %s", err) + logger.Warnf("error during read: %v", err) return } now := time.Now() sinceLastFrame := now.Sub(lastFrame) lastFrame = now - //fmt.Println("Video packet received", n, sinceLastFrame) if currentSession != nil { err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame}) if err != nil { - log.Println("Error writing sample", err) + logger.Warnf("error writing sample: %v", err) } } } @@ -250,7 +251,7 @@ func ExtractAndRunNativeBin() error { } }() - fmt.Printf("Binary started with PID: %d\n", cmd.Process.Pid) + logger.Infof("Binary started with PID: %d", cmd.Process.Pid) return nil } diff --git a/network.go b/network.go index 120f9f6..66b8616 100644 --- a/network.go +++ b/network.go @@ -56,14 +56,14 @@ func setDhcpClientState(active bool) { cmd := exec.Command("/usr/bin/killall", signal, "udhcpc") if err := cmd.Run(); err != nil { - fmt.Printf("network: setDhcpClientState: failed to change udhcpc state: %s\n", err) + logger.Warnf("network: setDhcpClientState: failed to change udhcpc state: %s", err) } } func checkNetworkState() { iface, err := netlink.LinkByName(NetIfName) if err != nil { - fmt.Printf("failed to get [%s] interface: %v\n", NetIfName, err) + logger.Warnf("failed to get [%s] interface: %v", NetIfName, err) return } @@ -76,7 +76,7 @@ func checkNetworkState() { addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL) if err != nil { - fmt.Printf("failed to get addresses for [%s]: %v\n", NetIfName, err) + logger.Warnf("failed to get addresses for [%s]: %v", NetIfName, err) } // If the link is going down, put udhcpc into idle mode. @@ -89,10 +89,10 @@ func checkNetworkState() { if addr.IP.To4() != nil { if !newState.Up && networkState.Up { // If the network is going down, remove all IPv4 addresses from the interface. - fmt.Printf("network: state transitioned to down, removing IPv4 address %s\n", addr.IP.String()) + logger.Infof("network: state transitioned to down, removing IPv4 address %s", addr.IP.String()) err := netlink.AddrDel(iface, &addr) if err != nil { - fmt.Printf("network: failed to delete %s", addr.IP.String()) + logger.Warnf("network: failed to delete %s", addr.IP.String()) } newState.IPv4 = "..." @@ -105,9 +105,9 @@ func checkNetworkState() { } if newState != networkState { - fmt.Println("network state changed") + logger.Info("network state changed") // restart MDNS - startMDNS() + _ = startMDNS() networkState = newState requestDisplayUpdate() } @@ -116,15 +116,15 @@ func checkNetworkState() { func startMDNS() error { // If server was previously running, stop it if mDNSConn != nil { - fmt.Printf("Stopping mDNS server\n") + logger.Info("Stopping mDNS server") err := mDNSConn.Close() if err != nil { - fmt.Printf("failed to stop mDNS server: %v\n", err) + logger.Warnf("failed to stop mDNS server: %v", err) } } // Start a new server - fmt.Printf("Starting mDNS server on jetkvm.local\n") + logger.Info("Starting mDNS server on jetkvm.local") addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) if err != nil { return err @@ -181,7 +181,7 @@ func getNTPServersFromDHCPInfo() ([]string, error) { for _, server := range strings.Fields(val) { if net.ParseIP(server) == nil { - fmt.Printf("invalid NTP server IP: %s, ignoring ... \n", server) + logger.Infof("invalid NTP server IP: %s, ignoring", server) } servers = append(servers, server) } @@ -190,11 +190,13 @@ func getNTPServersFromDHCPInfo() ([]string, error) { } func init() { + ensureConfigLoaded() + updates := make(chan netlink.LinkUpdate) done := make(chan struct{}) if err := netlink.LinkSubscribe(updates, done); err != nil { - fmt.Println("failed to subscribe to link updates: %v", err) + logger.Warnf("failed to subscribe to link updates: %v", err) return } @@ -208,7 +210,7 @@ func init() { select { case update := <-updates: if update.Link.Attrs().Name == NetIfName { - fmt.Printf("link update: %+v\n", update) + logger.Infof("link update: %+v", update) checkNetworkState() } case <-ticker.C: @@ -220,6 +222,6 @@ func init() { }() err := startMDNS() if err != nil { - fmt.Println("failed to run mDNS: %v", err) + logger.Warnf("failed to run mDNS: %v", err) } } diff --git a/ntp.go b/ntp.go index 92d0471..39ea7af 100644 --- a/ntp.go +++ b/ntp.go @@ -3,7 +3,6 @@ package kvm import ( "errors" "fmt" - "log" "net/http" "os/exec" "time" @@ -21,7 +20,6 @@ const ( ) var ( - timeSynced = false timeSyncRetryInterval = 0 * time.Second defaultNTPServers = []string{ "time.cloudflare.com", @@ -37,16 +35,16 @@ func TimeSyncLoop() { } if !networkState.Up { - log.Printf("Waiting for network to come up") + logger.Infof("Waiting for network to come up") time.Sleep(timeSyncWaitNetUpInt) continue } - log.Printf("Syncing system time") + logger.Infof("Syncing system time") start := time.Now() err := SyncSystemTime() if err != nil { - log.Printf("Failed to sync system time: %v", err) + logger.Warnf("Failed to sync system time: %v", err) // retry after a delay timeSyncRetryInterval += timeSyncRetryStep @@ -58,8 +56,7 @@ func TimeSyncLoop() { continue } - log.Printf("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start)) - timeSynced = true + logger.Infof("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start)) time.Sleep(timeSyncInterval) // after the first sync is done } } @@ -79,20 +76,20 @@ func SyncSystemTime() (err error) { func queryNetworkTime() (*time.Time, error) { ntpServers, err := getNTPServersFromDHCPInfo() if err != nil { - log.Printf("failed to get NTP servers from DHCP info: %v\n", err) + logger.Warnf("failed to get NTP servers from DHCP info: %v\n", err) } if ntpServers == nil { ntpServers = defaultNTPServers - log.Printf("Using default NTP servers: %v\n", ntpServers) + logger.Infof("Using default NTP servers: %v\n", ntpServers) } else { - log.Printf("Using NTP servers from DHCP: %v\n", ntpServers) + logger.Infof("Using NTP servers from DHCP: %v\n", ntpServers) } for _, server := range ntpServers { now, err := queryNtpServer(server, timeSyncTimeout) if err == nil { - log.Printf("NTP server [%s] returned time: %v\n", server, now) + logger.Infof("NTP server [%s] returned time: %v\n", server, now) return now, nil } } diff --git a/ota.go b/ota.go index 9f9cb6f..f813c09 100644 --- a/ota.go +++ b/ota.go @@ -8,7 +8,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "net/url" "os" @@ -77,7 +76,7 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease query.Set("prerelease", fmt.Sprintf("%v", includePreRelease)) updateUrl.RawQuery = query.Encode() - fmt.Println("Checking for updates at:", updateUrl.String()) + logger.Infof("Checking for updates at: %s", updateUrl) req, err := http.NewRequestWithContext(ctx, "GET", updateUrl.String(), nil) if err != nil { @@ -230,7 +229,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32) error } hashSum := hash.Sum(nil) - fmt.Printf("SHA256 hash of %s: %x\n", path, hashSum) + logger.Infof("SHA256 hash of %s: %x", path, hashSum) if hex.EncodeToString(hashSum) != expectedHash { return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash) @@ -272,7 +271,7 @@ var otaState = OTAState{} func triggerOTAStateUpdate() { go func() { if currentSession == nil { - log.Println("No active RPC session, skipping update state update") + logger.Info("No active RPC session, skipping update state update") return } writeJSONRPCEvent("otaState", otaState, currentSession) @@ -280,7 +279,7 @@ func triggerOTAStateUpdate() { } func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error { - log.Println("Trying to update...") + logger.Info("Trying to update...") if otaState.Updating { return fmt.Errorf("update already in progress") } @@ -315,7 +314,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err rebootNeeded := false if appUpdateAvailable { - fmt.Printf("App update available: %s -> %s\n", local.AppVersion, remote.AppVersion) + logger.Infof("App update available: %s -> %s", local.AppVersion, remote.AppVersion) err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress) if err != nil { @@ -341,14 +340,14 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err otaState.AppUpdateProgress = 1 triggerOTAStateUpdate() - fmt.Println("App update downloaded") + logger.Info("App update downloaded") rebootNeeded = true } else { - fmt.Println("App is up to date") + logger.Info("App is up to date") } if systemUpdateAvailable { - fmt.Printf("System update available: %s -> %s\n", local.SystemVersion, remote.SystemVersion) + logger.Infof("System update available: %s -> %s", local.SystemVersion, remote.SystemVersion) err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress) if err != nil { otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) @@ -366,7 +365,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err triggerOTAStateUpdate() return err } - fmt.Println("System update downloaded") + logger.Info("System update downloaded") verifyFinished := time.Now() otaState.SystemVerifiedAt = &verifyFinished otaState.SystemVerificationProgress = 1 @@ -413,17 +412,17 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output) } - fmt.Printf("rk_ota success, output: %s\n", output) + logger.Infof("rk_ota success, output: %s", output) otaState.SystemUpdateProgress = 1 otaState.SystemUpdatedAt = &verifyFinished triggerOTAStateUpdate() rebootNeeded = true } else { - fmt.Println("System is up to date") + logger.Info("System is up to date") } if rebootNeeded { - fmt.Println("System Rebooting in 10s...") + logger.Info("System Rebooting in 10s") time.Sleep(10 * time.Second) cmd := exec.Command("reboot") err := cmd.Start() diff --git a/prometheus.go b/prometheus.go new file mode 100644 index 0000000..8ebf259 --- /dev/null +++ b/prometheus.go @@ -0,0 +1,17 @@ +package kvm + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" + "github.com/prometheus/common/version" +) + +var promHandler http.Handler + +func initPrometheus() { + // A Prometheus metrics endpoint. + version.Version = builtAppVersion + prometheus.MustRegister(versioncollector.NewCollector("jetkvm")) +} diff --git a/remote_mount.go b/remote_mount.go index e6e7322..5b10695 100644 --- a/remote_mount.go +++ b/remote_mount.go @@ -49,7 +49,7 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) ( if err != nil { return nil, err } - buf := make([]byte, 0) + var buf []byte for { select { case data := <-diskReadChan: diff --git a/serial.go b/serial.go index 3ad56d3..a4ab7d5 100644 --- a/serial.go +++ b/serial.go @@ -16,14 +16,14 @@ const serialPortPath = "/dev/ttyS3" var port serial.Port func mountATXControl() error { - port.SetMode(defaultMode) + _ = port.SetMode(defaultMode) go runATXControl() return nil } func unmountATXControl() error { - reopenSerialPort() + _ = reopenSerialPort() return nil } @@ -122,13 +122,13 @@ func pressATXResetButton(duration time.Duration) error { } func mountDCControl() error { - port.SetMode(defaultMode) + _ = port.SetMode(defaultMode) go runDCControl() return nil } func unmountDCControl() error { - reopenSerialPort() + _ = reopenSerialPort() return nil } @@ -212,11 +212,11 @@ var defaultMode = &serial.Mode{ } func initSerialPort() { - reopenSerialPort() + _ = reopenSerialPort() if config.ActiveExtension == "atx-power" { - mountATXControl() + _ = mountATXControl() } else if config.ActiveExtension == "dc-power" { - mountDCControl() + _ = mountDCControl() } } diff --git a/terminal.go b/terminal.go index 1a1ac1c..3e64020 100644 --- a/terminal.go +++ b/terminal.go @@ -55,11 +55,13 @@ func handleTerminalChannel(d *webrtc.DataChannel) { var size TerminalSize err := json.Unmarshal([]byte(msg.Data), &size) if err == nil { - pty.Setsize(ptmx, &pty.Winsize{ + err = pty.Setsize(ptmx, &pty.Winsize{ Rows: uint16(size.Rows), Cols: uint16(size.Cols), }) - return + if err == nil { + return + } } logger.Errorf("Failed to parse terminal size: %v", err) } @@ -74,7 +76,7 @@ func handleTerminalChannel(d *webrtc.DataChannel) { ptmx.Close() } if cmd != nil && cmd.Process != nil { - cmd.Process.Kill() + _ = cmd.Process.Kill() } }) } diff --git a/ui/.env.cloud-development b/ui/.env.cloud-development new file mode 100644 index 0000000..471e280 --- /dev/null +++ b/ui/.env.cloud-development @@ -0,0 +1,4 @@ +# 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=http://localhost:3000 diff --git a/ui/.env.cloud-production b/ui/.env.cloud-production new file mode 100644 index 0000000..d9895d2 --- /dev/null +++ b/ui/.env.cloud-production @@ -0,0 +1,4 @@ +# 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 new file mode 100644 index 0000000..bc5c14c --- /dev/null +++ b/ui/.env.cloud-staging @@ -0,0 +1,4 @@ +# 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/.env.development b/ui/.env.development deleted file mode 100644 index 172328c..0000000 --- a/ui/.env.development +++ /dev/null @@ -1,6 +0,0 @@ -VITE_SIGNAL_API=http://localhost:3000 - -VITE_CLOUD_APP=http://localhost:5173 -VITE_CLOUD_API=http://localhost:3000 - -VITE_JETKVM_HEAD= \ No newline at end of file diff --git a/ui/.env.device b/ui/.env.device deleted file mode 100644 index 2aaa6a7..0000000 --- a/ui/.env.device +++ /dev/null @@ -1,6 +0,0 @@ -VITE_SIGNAL_API= # Uses the KVM device's IP address as the signal API endpoint - -VITE_CLOUD_APP=https://app.jetkvm.com -VITE_CLOUD_API=https://api.jetkvm.com - -VITE_JETKVM_HEAD= \ No newline at end of file diff --git a/ui/.env.production b/ui/.env.production deleted file mode 100644 index 2587c0c..0000000 --- a/ui/.env.production +++ /dev/null @@ -1,6 +0,0 @@ -VITE_SIGNAL_API=https://api.jetkvm.com - -VITE_CLOUD_APP=https://app.jetkvm.com -VITE_CLOUD_API=https://api.jetkvm.com - -VITE_JETKVM_HEAD= \ No newline at end of file diff --git a/ui/.env.staging b/ui/.env.staging deleted file mode 100644 index 651e5bc..0000000 --- a/ui/.env.staging +++ /dev/null @@ -1,4 +0,0 @@ -VITE_SIGNAL_API=https://staging-api.jetkvm.com - -VITE_CLOUD_APP=https://staging-app.jetkvm.com -VITE_CLOUD_API=https://staging-api.jetkvm.com \ No newline at end of file diff --git a/ui/dev_device.sh b/ui/dev_device.sh index 2fa8e4e..092b8c8 100755 --- a/ui/dev_device.sh +++ b/ui/dev_device.sh @@ -1,21 +1,19 @@ #!/bin/bash +# Check if an IP address was provided as an argument +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +ip_address="$1" + # Print header echo "┌──────────────────────────────────────┐" echo "│ JetKVM Development Setup │" echo "└──────────────────────────────────────┘" -# Prompt for IP address -printf "Please enter the IP address of your JetKVM device: " -read ip_address - -# Validate input is not empty -if [ -z "$ip_address" ]; then - echo "Error: IP address cannot be empty" - exit 1 -fi - # Set the environment variable and run Vite echo "Starting development server with JetKVM device at: $ip_address" sleep 1 -JETKVM_PROXY_URL="http://$ip_address" vite dev --mode=device +JETKVM_PROXY_URL="http://$ip_address" npx vite dev --mode=device diff --git a/ui/index.html b/ui/index.html index 72d2594..af9bdfb 100644 --- a/ui/index.html +++ b/ui/index.html @@ -28,7 +28,6 @@ JetKVM - %VITE_JETKVM_HEAD%