diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b49c4121..9a7ceb76 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -449,13 +449,14 @@ export JETKVM_PROXY_URL="ws://" ### Performance Profiling -```bash -# Enable profiling -go build -o bin/jetkvm_app -ldflags="-X main.enableProfiling=true" cmd/main.go +1. Enable `Developer Mode` on your JetKVM device +2. Add a password on the `Access` tab +```bash # Access profiling -curl http://:6060/debug/pprof/ +curl http://api:$JETKVM_PASSWORD@YOUR_DEVICE_IP/developer/pprof/ ``` + ### Advanced Environment Variables ```bash diff --git a/Makefile b/Makefile index 1badffe8..25a5e0fa 100644 --- a/Makefile +++ b/Makefile @@ -102,14 +102,17 @@ build_dev_test: build_audio_deps build_test2json build_gotestsum frontend: cd ui && npm ci && npm run build:device && \ - find ../static/assets \ + find ../static/ \ -type f \ \( -name '*.js' \ -o -name '*.css' \ + -o -name '*.html' \ + -o -name '*.ico' \ -o -name '*.png' \ -o -name '*.jpg' \ -o -name '*.jpeg' \ -o -name '*.gif' \ + -o -name '*.svg' \ -o -name '*.webp' \ -o -name '*.woff2' \ \) \ diff --git a/display.go b/display.go index f34a6486..15d3ffcf 100644 --- a/display.go +++ b/display.go @@ -1,6 +1,7 @@ package kvm import ( + "context" "errors" "fmt" "os" @@ -110,12 +111,6 @@ func clearDisplayState() { currentScreen = "ui_Boot_Screen" } -var ( - cloudBlinkLock sync.Mutex = sync.Mutex{} - cloudBlinkStopped bool - cloudBlinkTicker *time.Ticker -) - func updateDisplay() { updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4String()) if usbState == "configured" { @@ -152,48 +147,81 @@ func updateDisplay() { stopCloudBlink() case CloudConnectionStateConnecting: _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png") - startCloudBlink() + restartCloudBlink() case CloudConnectionStateConnected: _, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png") stopCloudBlink() } } -func startCloudBlink() { - if cloudBlinkTicker == nil { - cloudBlinkTicker = time.NewTicker(2 * time.Second) - } else { - // do nothing if the blink isn't stopped - if cloudBlinkStopped { - cloudBlinkLock.Lock() - defer cloudBlinkLock.Unlock() +const ( + cloudBlinkInterval = 2 * time.Second + cloudBlinkDuration = 1 * time.Second +) - cloudBlinkStopped = false - cloudBlinkTicker.Reset(2 * time.Second) +var ( + cloudBlinkTicker *time.Ticker + cloudBlinkCancel context.CancelFunc + cloudBlinkLock = sync.Mutex{} +) + +func doCloudBlink(ctx context.Context) { + for range cloudBlinkTicker.C { + if cloudConnectionState != CloudConnectionStateConnecting { + continue + } + + _, _ = lvObjFadeOut("ui_Home_Header_Cloud_Status_Icon", uint32(cloudBlinkDuration.Milliseconds())) + + select { + case <-ctx.Done(): + return + case <-time.After(cloudBlinkDuration): + } + + _, _ = lvObjFadeIn("ui_Home_Header_Cloud_Status_Icon", uint32(cloudBlinkDuration.Milliseconds())) + + select { + case <-ctx.Done(): + return + case <-time.After(cloudBlinkDuration): } } +} - go func() { - for range cloudBlinkTicker.C { - if cloudConnectionState != CloudConnectionStateConnecting { - continue - } - _, _ = lvObjFadeOut("ui_Home_Header_Cloud_Status_Icon", 1000) - time.Sleep(1000 * time.Millisecond) - _, _ = lvObjFadeIn("ui_Home_Header_Cloud_Status_Icon", 1000) - time.Sleep(1000 * time.Millisecond) - } - }() +func restartCloudBlink() { + stopCloudBlink() + startCloudBlink() +} + +func startCloudBlink() { + cloudBlinkLock.Lock() + defer cloudBlinkLock.Unlock() + + if cloudBlinkTicker == nil { + cloudBlinkTicker = time.NewTicker(cloudBlinkInterval) + } else { + cloudBlinkTicker.Reset(cloudBlinkInterval) + } + + ctx, cancel := context.WithCancel(context.Background()) + cloudBlinkCancel = cancel + + go doCloudBlink(ctx) } func stopCloudBlink() { + cloudBlinkLock.Lock() + defer cloudBlinkLock.Unlock() + + if cloudBlinkCancel != nil { + cloudBlinkCancel() + cloudBlinkCancel = nil + } + if cloudBlinkTicker != nil { cloudBlinkTicker.Stop() } - - cloudBlinkLock.Lock() - defer cloudBlinkLock.Unlock() - cloudBlinkStopped = true } var ( diff --git a/resource/jetkvm_native b/resource/jetkvm_native index a47288b9..68d0d4e0 100644 Binary files a/resource/jetkvm_native and b/resource/jetkvm_native differ diff --git a/resource/jetkvm_native.sha256 b/resource/jetkvm_native.sha256 index ceba8b20..0c0a4ff5 100644 --- a/resource/jetkvm_native.sha256 +++ b/resource/jetkvm_native.sha256 @@ -1 +1 @@ -6dabd0e657dd099280d9173069687786a4a8c9c25cf7f9e7ce2f940cab67c521 +01db2bbcd0bad46c3e21eb3cc5687d15df2153c3d8e2d4665b37acb55f0b5a57 diff --git a/resource/netboot.xyz-multiarch.iso b/resource/netboot.xyz-multiarch.iso index c3a4527f..2691c728 100644 Binary files a/resource/netboot.xyz-multiarch.iso and b/resource/netboot.xyz-multiarch.iso differ diff --git a/scripts/update_netboot_xyz.sh b/scripts/update_netboot_xyz.sh new file mode 100755 index 00000000..901d47f3 --- /dev/null +++ b/scripts/update_netboot_xyz.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# +# Exit immediately if a command exits with a non-zero status +set -e + +C_RST="$(tput sgr0)" +C_ERR="$(tput setaf 1)" +C_OK="$(tput setaf 2)" +C_WARN="$(tput setaf 3)" +C_INFO="$(tput setaf 5)" + +msg() { printf '%s%s%s\n' $2 "$1" $C_RST; } + +msg_info() { msg "$1" $C_INFO; } +msg_ok() { msg "$1" $C_OK; } +msg_err() { msg "$1" $C_ERR; } +msg_warn() { msg "$1" $C_WARN; } + +# Get the latest release information +msg_info "Getting latest release information ..." +LATEST_RELEASE=$(curl -s \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/netbootxyz/netboot.xyz/releases | jq ' + [.[] | select(.prerelease == false and .draft == false and .assets != null and (.assets | length > 0))] | + sort_by(.created_at) | + .[-1]') + +# Extract version, download URL, and digest +VERSION=$(echo "$LATEST_RELEASE" | jq -r '.tag_name') +ISO_URL=$(echo "$LATEST_RELEASE" | jq -r '.assets[] | select(.name == "netboot.xyz-multiarch.iso") | .browser_download_url') +EXPECTED_CHECKSUM=$(echo "$LATEST_RELEASE" | jq -r '.assets[] | select(.name == "netboot.xyz-multiarch.iso") | .digest' | sed 's/sha256://') + +msg_ok "Latest version: $VERSION" +msg_ok "ISO URL: $ISO_URL" +msg_ok "Expected SHA256: $EXPECTED_CHECKSUM" + + +# Check if we already have the same version +if [ -f "resource/netboot.xyz-multiarch.iso" ]; then + msg_info "Checking current resource file ..." + + # First check by checksum (fastest) + CURRENT_CHECKSUM=$(shasum -a 256 resource/netboot.xyz-multiarch.iso | awk '{print $1}') + + if [ "$CURRENT_CHECKSUM" = "$EXPECTED_CHECKSUM" ]; then + msg_ok "Resource file is already up to date (version $VERSION). No update needed." + exit 0 + else + msg_info "Checksums differ, proceeding with download ..." + fi +fi + +# Download ISO file +TMP_ISO=$(mktemp -t netbootxyziso) +msg_info "Downloading ISO file ..." +curl -L -o "$TMP_ISO" "$ISO_URL" + +# Verify SHA256 checksum +msg_info "Verifying SHA256 checksum ..." +ACTUAL_CHECKSUM=$(shasum -a 256 "$TMP_ISO" | awk '{print $1}') + +if [ "$EXPECTED_CHECKSUM" = "$ACTUAL_CHECKSUM" ]; then + msg_ok "Verified SHA256 checksum." + mv -f "$TMP_ISO" "resource/netboot.xyz-multiarch.iso" + msg_ok "Updated ISO file." + git add "resource/netboot.xyz-multiarch.iso" + git commit -m "chore: update netboot.xyz-multiarch.iso to $VERSION" + msg_ok "Committed changes." + msg_ok "You can now push the changes to the remote repository." + exit 0 +else + msg_err "Inconsistent SHA256 checksum." + msg_err "Expected: $EXPECTED_CHECKSUM" + msg_err "Actual: $ACTUAL_CHECKSUM" + exit 1 +fi \ No newline at end of file diff --git a/ui/index.html b/ui/index.html index 0ce91234..a798221c 100644 --- a/ui/index.html +++ b/ui/index.html @@ -6,27 +6,34 @@ + JetKVM - + @@ -36,23 +43,21 @@ { esbuild: { pure: ["console.debug"], }, + assetsInclude: ["**/*.woff2"], build: { outDir: isCloud ? "dist" : "../static", rollupOptions: { diff --git a/web.go b/web.go index 7ef5547c..7b1d0ad4 100644 --- a/web.go +++ b/web.go @@ -69,8 +69,7 @@ type SetupRequest struct { } var cachableFileExtensions = []string{ - ".jpg", ".jpeg", ".png", ".gif", ".webp", ".woff2", - ".ico", + ".jpg", ".jpeg", ".png", ".svg", ".gif", ".webp", ".ico", ".woff2", } func setupRouter() *gin.Engine { @@ -83,7 +82,10 @@ func setupRouter() *gin.Engine { }), )) - staticFS, _ := fs.Sub(staticFiles, "static") + staticFS, err := fs.Sub(staticFiles, "static") + if err != nil { + logger.Fatal().Err(err).Msg("failed to get rooted static files subdirectory") + } staticFileServer := http.StripPrefix("/static", statigz.FileServer( staticFS.(fs.ReadDirFS), )) @@ -109,9 +111,17 @@ func setupRouter() *gin.Engine { c.Next() }) + r.GET("/robots.txt", func(c *gin.Context) { + c.Header("Content-Type", "text/plain") + c.Header("Cache-Control", "public, max-age=31536000, immutable") // Cache for 1 year + c.String(http.StatusOK, "User-agent: *\nDisallow: /") + }) + r.Any("/static/*w", func(c *gin.Context) { staticFileServer.ServeHTTP(c.Writer, c.Request) }) + + // Public routes (no authentication required) r.POST("/auth/login-local", handleLogin) // We use this to determine if the device is setup