mirror of https://github.com/jetkvm/kvm.git
Compare commits
5 Commits
ce92ddf717
...
a2a8d99397
| Author | SHA1 | Date |
|---|---|---|
|
|
a2a8d99397 | |
|
|
37b1a8bf34 | |
|
|
ca8b06f4cf | |
|
|
33e099f258 | |
|
|
ba23da6973 |
|
|
@ -301,13 +301,14 @@ export JETKVM_PROXY_URL="ws://<IP>"
|
|||
|
||||
### 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://<IP>:6060/debug/pprof/
|
||||
curl http://api:$JETKVM_PASSWORD@YOUR_DEVICE_IP/developer/pprof/
|
||||
```
|
||||
|
||||
### Advanced Environment Variables
|
||||
|
||||
```bash
|
||||
|
|
|
|||
5
Makefile
5
Makefile
|
|
@ -63,14 +63,17 @@ build_dev_test: 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' \
|
||||
\) \
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ type NetworkInterfaceOptions struct {
|
|||
DefaultHostname string
|
||||
OnStateChange func(state *NetworkInterfaceState)
|
||||
OnInitialCheck func(state *NetworkInterfaceState)
|
||||
OnDhcpLeaseChange func(lease *udhcpc.Lease)
|
||||
OnDhcpLeaseChange func(lease *udhcpc.Lease, state *NetworkInterfaceState)
|
||||
OnConfigChange func(config *NetworkConfig)
|
||||
NetworkConfig *NetworkConfig
|
||||
}
|
||||
|
|
@ -94,7 +94,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
|||
_ = s.updateNtpServersFromLease(lease)
|
||||
_ = s.setHostnameIfNotSame()
|
||||
|
||||
opts.OnDhcpLeaseChange(lease)
|
||||
opts.OnDhcpLeaseChange(lease, s)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -9,17 +9,32 @@ import (
|
|||
"github.com/beevik/ntp"
|
||||
)
|
||||
|
||||
var defaultNTPServers = []string{
|
||||
var defaultNTPServerIPs = []string{
|
||||
// These servers are known by static IP and as such don't need DNS lookups
|
||||
// These are from Google and Cloudflare since if they're down, the internet
|
||||
// is broken anyway
|
||||
"162.159.200.1", // time.cloudflare.com IPv4
|
||||
"162.159.200.123", // time.cloudflare.com IPv4
|
||||
"2606:4700:f1::1", // time.cloudflare.com IPv6
|
||||
"2606:4700:f1::123", // time.cloudflare.com IPv6
|
||||
"216.239.35.0", // time.google.com IPv4
|
||||
"216.239.35.4", // time.google.com IPv4
|
||||
"216.239.35.8", // time.google.com IPv4
|
||||
"216.239.35.12", // time.google.com IPv4
|
||||
"2001:4860:4806::", // time.google.com IPv6
|
||||
"2001:4860:4806:4::", // time.google.com IPv6
|
||||
"2001:4860:4806:8::", // time.google.com IPv6
|
||||
"2001:4860:4806:c::", // time.google.com IPv6
|
||||
}
|
||||
|
||||
var defaultNTPServerHostnames = []string{
|
||||
// should use something from https://github.com/jauderho/public-ntp-servers
|
||||
"time.apple.com",
|
||||
"time.aws.com",
|
||||
"time.windows.com",
|
||||
"time.google.com",
|
||||
"162.159.200.123", // time.cloudflare.com IPv4
|
||||
"2606:4700:f1::123", // time.cloudflare.com IPv6
|
||||
"0.pool.ntp.org",
|
||||
"1.pool.ntp.org",
|
||||
"2.pool.ntp.org",
|
||||
"3.pool.ntp.org",
|
||||
"time.cloudflare.com",
|
||||
"pool.ntp.org",
|
||||
}
|
||||
|
||||
func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) {
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ func (t *TimeSync) Sync() error {
|
|||
var (
|
||||
now *time.Time
|
||||
offset *time.Duration
|
||||
log zerolog.Logger
|
||||
)
|
||||
|
||||
metricTimeSyncCount.Inc()
|
||||
|
|
@ -166,54 +167,54 @@ func (t *TimeSync) Sync() error {
|
|||
|
||||
Orders:
|
||||
for _, mode := range syncMode.Ordering {
|
||||
log = t.l.With().Str("mode", mode).Logger()
|
||||
switch mode {
|
||||
case "ntp_user_provided":
|
||||
if syncMode.Ntp {
|
||||
t.l.Info().Msg("using NTP custom servers")
|
||||
log.Info().Msg("using NTP custom servers")
|
||||
now, offset = t.queryNetworkTime(t.networkConfig.TimeSyncNTPServers)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "NTP").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "ntp_dhcp":
|
||||
if syncMode.Ntp {
|
||||
t.l.Info().Msg("using NTP servers from DHCP")
|
||||
log.Info().Msg("using NTP servers from DHCP")
|
||||
now, offset = t.queryNetworkTime(t.dhcpNtpAddresses)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "NTP DHCP").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "ntp":
|
||||
if syncMode.Ntp && syncMode.NtpUseFallback {
|
||||
t.l.Info().Msg("using NTP fallback")
|
||||
now, offset = t.queryNetworkTime(defaultNTPServers)
|
||||
log.Info().Msg("using NTP fallback IPs")
|
||||
now, offset = t.queryNetworkTime(defaultNTPServerIPs)
|
||||
if now == nil {
|
||||
log.Info().Msg("using NTP fallback hostnames")
|
||||
now, offset = t.queryNetworkTime(defaultNTPServerHostnames)
|
||||
}
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "NTP fallback").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "http_user_provided":
|
||||
if syncMode.Http {
|
||||
t.l.Info().Msg("using HTTP custom URLs")
|
||||
log.Info().Msg("using HTTP custom URLs")
|
||||
now = t.queryAllHttpTime(t.networkConfig.TimeSyncHTTPUrls)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "HTTP").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "http":
|
||||
if syncMode.Http && syncMode.HttpUseFallback {
|
||||
t.l.Info().Msg("using HTTP fallback")
|
||||
log.Info().Msg("using HTTP fallback")
|
||||
now = t.queryAllHttpTime(defaultHTTPUrls)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "HTTP fallback").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.l.Warn().Str("mode", mode).Msg("unknown time sync mode, skipping")
|
||||
log.Warn().Msg("unknown time sync mode, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,6 +227,8 @@ Orders:
|
|||
now = &newNow
|
||||
}
|
||||
|
||||
log.Info().Time("now", *now).Msg("time obtained")
|
||||
|
||||
err := t.setSystemTime(*now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set system time: %w", err)
|
||||
|
|
|
|||
9
main.go
9
main.go
|
|
@ -96,16 +96,25 @@ func Main() {
|
|||
if !config.AutoUpdateEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
|
||||
logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
if currentSession != nil {
|
||||
logger.Debug().Msg("skipping update since a session is active")
|
||||
time.Sleep(1 * time.Minute)
|
||||
continue
|
||||
}
|
||||
|
||||
includePreRelease := config.IncludePreRelease
|
||||
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to auto update")
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Hour)
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
27
network.go
27
network.go
|
|
@ -15,7 +15,7 @@ var (
|
|||
networkState *network.NetworkInterfaceState
|
||||
)
|
||||
|
||||
func networkStateChanged() {
|
||||
func networkStateChanged(isOnline bool) {
|
||||
// do not block the main thread
|
||||
go waitCtrlAndRequestDisplayUpdate(true)
|
||||
|
||||
|
|
@ -37,6 +37,13 @@ func networkStateChanged() {
|
|||
networkState.GetFQDN(),
|
||||
}, true)
|
||||
}
|
||||
|
||||
// if the network is now online, trigger an NTP sync if still needed
|
||||
if isOnline && timeSync != nil && (isTimeSyncNeeded() || !timeSync.IsSyncSuccess()) {
|
||||
if err := timeSync.Sync(); err != nil {
|
||||
logger.Warn().Str("error", err.Error()).Msg("unable to sync time on network state change")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initNetwork() error {
|
||||
|
|
@ -48,13 +55,13 @@ func initNetwork() error {
|
|||
NetworkConfig: config.NetworkConfig,
|
||||
Logger: networkLogger,
|
||||
OnStateChange: func(state *network.NetworkInterfaceState) {
|
||||
networkStateChanged()
|
||||
networkStateChanged(state.IsOnline())
|
||||
},
|
||||
OnInitialCheck: func(state *network.NetworkInterfaceState) {
|
||||
networkStateChanged()
|
||||
networkStateChanged(state.IsOnline())
|
||||
},
|
||||
OnDhcpLeaseChange: func(lease *udhcpc.Lease) {
|
||||
networkStateChanged()
|
||||
OnDhcpLeaseChange: func(lease *udhcpc.Lease, state *network.NetworkInterfaceState) {
|
||||
networkStateChanged(state.IsOnline())
|
||||
|
||||
if currentSession == nil {
|
||||
return
|
||||
|
|
@ -64,7 +71,15 @@ func initNetwork() error {
|
|||
},
|
||||
OnConfigChange: func(networkConfig *network.NetworkConfig) {
|
||||
config.NetworkConfig = networkConfig
|
||||
networkStateChanged()
|
||||
networkStateChanged(false)
|
||||
|
||||
if mDNS != nil {
|
||||
_ = mDNS.SetListenOptions(networkConfig.GetMDNSMode())
|
||||
_ = mDNS.SetLocalNames([]string{
|
||||
networkState.GetHostname(),
|
||||
networkState.GetFQDN(),
|
||||
}, true)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -6,27 +6,34 @@
|
|||
<!-- These are the fonts used in the app -->
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/CircularXXWeb-Medium.woff2"
|
||||
href="./public/fonts/CircularXXWeb-Medium.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/CircularXXWeb-Book.woff2"
|
||||
href="./public/fonts/CircularXXWeb-Book.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/CircularXXWeb-Regular.woff2"
|
||||
href="./public/fonts/CircularXXWeb-Regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="./public/fonts/CircularXXWeb-Black.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<title>JetKVM</title>
|
||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||
<link rel="stylesheet" href="./public/fonts/fonts.css" />
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
|
|
@ -36,23 +43,21 @@
|
|||
<meta name="theme-color" content="#051946" />
|
||||
<meta name="description" content="A web-based KVM console for managing remote servers." />
|
||||
<script>
|
||||
// Initial theme setup
|
||||
document.documentElement.classList.toggle(
|
||||
"dark",
|
||||
localStorage.theme === "dark" ||
|
||||
function applyThemeFromPreference() {
|
||||
// dark theme setup
|
||||
var darkDesired = localStorage.theme === "dark" ||
|
||||
(!("theme" in localStorage) &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches),
|
||||
);
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
|
||||
document.documentElement.classList.toggle("dark", darkDesired)
|
||||
}
|
||||
|
||||
// initial theme application
|
||||
applyThemeFromPreference();
|
||||
|
||||
// Listen for system theme changes
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", ({ matches }) => {
|
||||
if (!("theme" in localStorage)) {
|
||||
// Only auto-switch if user hasn't manually set a theme
|
||||
document.documentElement.classList.toggle("dark", matches);
|
||||
}
|
||||
});
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyThemeFromPreference);
|
||||
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", applyThemeFromPreference);
|
||||
</script>
|
||||
</head>
|
||||
<body
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
||||
|
|
@ -31,6 +31,7 @@ export default defineConfig(({ mode, command }) => {
|
|||
esbuild: {
|
||||
pure: ["console.debug"],
|
||||
},
|
||||
assetsInclude: ["**/*.woff2"],
|
||||
build: {
|
||||
outDir: isCloud ? "dist" : "../static",
|
||||
rollupOptions: {
|
||||
|
|
|
|||
16
web.go
16
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue