This commit is contained in:
Jordan Jones 2025-01-06 15:13:47 -08:00 committed by GitHub
commit a1f6557c43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 576 additions and 517 deletions

22
.editorconfig Normal file
View File

@ -0,0 +1,22 @@
; https://editorconfig.org/
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
indent_style = tab
indent_size = 4
[*.md]
indent_size = 4
eclint_indent_style = unset
[Dockerfile]
indent_size = 4

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -1,9 +1,90 @@
package main package main
import ( import (
"kvm" "context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/jetkvm/kvm/internal/config"
"github.com/jetkvm/kvm/internal/kvm"
"github.com/jetkvm/kvm/internal/logging"
"github.com/gwatts/rootcerts"
) )
var ctx context.Context
func main() { func main() {
kvm.Main() var cancel context.CancelFunc
ctx, cancel = context.WithCancel(context.Background())
defer cancel()
logging.Logger.Info("Starting JetKvm")
go kvm.RunWatchdog(ctx)
go kvm.ConfirmCurrentSystem()
http.DefaultClient.Timeout = 1 * time.Minute
cfg := config.LoadConfig()
logging.Logger.Debug("config loaded")
err := rootcerts.UpdateDefaultTransport()
if err != nil {
logging.Logger.Errorf("failed to load CA certs: %v", err)
}
go kvm.TimeSyncLoop()
kvm.StartNativeCtrlSocketServer()
kvm.StartNativeVideoSocketServer()
go func() {
err = kvm.ExtractAndRunNativeBin(ctx)
if err != nil {
logging.Logger.Errorf("failed to extract and run native bin: %v", err)
//TODO: prepare an error message screen buffer to show on kvm screen
}
}()
go func() {
time.Sleep(15 * time.Minute)
for {
logging.Logger.Debugf("UPDATING - Auto update enabled: %v", cfg.AutoUpdateEnabled)
if cfg.AutoUpdateEnabled == false {
return
}
if kvm.CurrentSession != nil {
logging.Logger.Debugf("skipping update since a session is active")
time.Sleep(1 * time.Minute)
continue
}
includePreRelease := cfg.IncludePreRelease
err = kvm.TryUpdate(context.Background(), kvm.GetDeviceID(), includePreRelease)
if err != nil {
logging.Logger.Errorf("failed to auto update: %v", err)
}
time.Sleep(1 * time.Hour)
}
}()
//go RunFuseServer()
go kvm.RunWebServer()
go kvm.RunWebsocketClient()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
log.Println("JetKVM Shutting Down")
//if fuseServer != nil {
// err := setMassStorageImage(" ")
// if err != nil {
// log.Printf("Failed to unmount mass storage image: %v", err)
// }
// err = fuseServer.Unmount()
// if err != nil {
// log.Printf("Failed to unmount fuse: %v", err)
// }
// os.Exit(0)
} }

0
dev_deploy.sh Executable file → Normal file
View File

2
go.mod
View File

@ -1,4 +1,4 @@
module kvm module github.com/jetkvm/kvm
go 1.21.0 go 1.21.0

View File

@ -1,9 +1,11 @@
package kvm package config
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"github.com/jetkvm/kvm/internal/logging"
) )
type WakeOnLanDevice struct { type WakeOnLanDevice struct {
@ -33,30 +35,31 @@ var defaultConfig = &Config{
var config *Config var config *Config
func LoadConfig() { func LoadConfig() *Config {
if config != nil { if config != nil {
return return config
} }
file, err := os.Open(configPath) file, err := os.Open(configPath)
if err != nil { if err != nil {
logger.Debug("default config file doesn't exist, using default") logging.Logger.Debug("default config file doesn't exist, using default")
config = defaultConfig config = defaultConfig
return return config
} }
defer file.Close() defer file.Close()
var loadedConfig Config var loadedConfig Config
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil { if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
logger.Errorf("config file JSON parsing failed, %v", err) logging.Logger.Errorf("config file JSON parsing failed, %v", err)
config = defaultConfig config = defaultConfig
return return config
} }
config = &loadedConfig config = &loadedConfig
return config
} }
func SaveConfig() error { func SaveConfig(cfg *Config) error {
file, err := os.Create(configPath) file, err := os.Create(configPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create config file: %w", err) return fmt.Errorf("failed to create config file: %w", err)
@ -65,7 +68,7 @@ func SaveConfig() error {
encoder := json.NewEncoder(file) encoder := json.NewEncoder(file)
encoder.SetIndent("", " ") encoder.SetIndent("", " ")
if err := encoder.Encode(config); err != nil { if err := encoder.Encode(cfg); err != nil {
return fmt.Errorf("failed to encode config: %w", err) return fmt.Errorf("failed to encode config: %w", err)
} }

View File

@ -8,6 +8,7 @@ import (
"os" "os"
"time" "time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/pojntfx/go-nbd/pkg/client" "github.com/pojntfx/go-nbd/pkg/client"
"github.com/pojntfx/go-nbd/pkg/server" "github.com/pojntfx/go-nbd/pkg/server"
) )
@ -16,15 +17,15 @@ type remoteImageBackend struct {
} }
func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) { func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) {
virtualMediaStateMutex.RLock() VirtualMediaStateMutex.RLock()
logger.Debugf("currentVirtualMediaState is %v", currentVirtualMediaState) logging.Logger.Debugf("currentVirtualMediaState is %v", CurrentVirtualMediaState)
logger.Debugf("read size: %d, off: %d", len(p), off) logging.Logger.Debugf("read size: %d, off: %d", len(p), off)
if currentVirtualMediaState == nil { if CurrentVirtualMediaState == nil {
return 0, errors.New("image not mounted") return 0, errors.New("image not mounted")
} }
source := currentVirtualMediaState.Source source := CurrentVirtualMediaState.Source
mountedImageSize := currentVirtualMediaState.Size mountedImageSize := CurrentVirtualMediaState.Size
virtualMediaStateMutex.RUnlock() VirtualMediaStateMutex.RUnlock()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
@ -35,14 +36,14 @@ func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) {
} }
var data []byte var data []byte
if source == WebRTC { if source == WebRTC {
data, err = webRTCDiskReader.Read(ctx, off, readLen) data, err = WebRTCDiskReader.Read(ctx, off, readLen)
if err != nil { if err != nil {
return 0, err return 0, err
} }
n = copy(p, data) n = copy(p, data)
return n, nil return n, nil
} else if source == HTTP { } else if source == HTTP {
return httpRangeReader.ReadAt(p, off) return HttpRangeReader.ReadAt(p, off)
} else { } else {
return 0, errors.New("unknown image source") return 0, errors.New("unknown image source")
} }
@ -53,12 +54,12 @@ func (r remoteImageBackend) WriteAt(p []byte, off int64) (n int, err error) {
} }
func (r remoteImageBackend) Size() (int64, error) { func (r remoteImageBackend) Size() (int64, error) {
virtualMediaStateMutex.Lock() VirtualMediaStateMutex.Lock()
defer virtualMediaStateMutex.Unlock() defer VirtualMediaStateMutex.Unlock()
if currentVirtualMediaState == nil { if CurrentVirtualMediaState == nil {
return 0, errors.New("no virtual media state") return 0, errors.New("no virtual media state")
} }
return currentVirtualMediaState.Size, nil return CurrentVirtualMediaState.Size, nil
} }
func (r remoteImageBackend) Sync() error { func (r remoteImageBackend) Sync() error {

View File

@ -7,13 +7,16 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"github.com/coder/websocket/wsjson"
"time" "time"
"github.com/coder/websocket/wsjson"
"github.com/jetkvm/kvm/internal/config"
"github.com/jetkvm/kvm/internal/logging"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"github.com/coder/websocket" "github.com/coder/websocket"
"github.com/gin-gonic/gin"
) )
type CloudRegisterRequest struct { type CloudRegisterRequest struct {
@ -23,7 +26,7 @@ type CloudRegisterRequest struct {
ClientId string `json:"clientId"` ClientId string `json:"clientId"`
} }
func handleCloudRegister(c *gin.Context) { func HandleCloudRegister(c *gin.Context) {
var req CloudRegisterRequest var req CloudRegisterRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@ -68,8 +71,10 @@ func handleCloudRegister(c *gin.Context) {
return return
} }
config.CloudToken = tokenResp.SecretToken cfg := config.LoadConfig()
config.CloudURL = req.CloudAPI
cfg.CloudToken = tokenResp.SecretToken
cfg.CloudURL = req.CloudAPI
provider, err := oidc.NewProvider(c, "https://accounts.google.com") provider, err := oidc.NewProvider(c, "https://accounts.google.com")
if err != nil { if err != nil {
@ -88,10 +93,10 @@ func handleCloudRegister(c *gin.Context) {
return return
} }
config.GoogleIdentity = idToken.Audience[0] + ":" + idToken.Subject cfg.GoogleIdentity = idToken.Audience[0] + ":" + idToken.Subject
// Save the updated configuration // Save the updated configuration
if err := SaveConfig(); err != nil { if err := config.SaveConfig(cfg); err != nil {
c.JSON(500, gin.H{"error": "Failed to save configuration"}) c.JSON(500, gin.H{"error": "Failed to save configuration"})
return return
} }
@ -100,11 +105,12 @@ func handleCloudRegister(c *gin.Context) {
} }
func runWebsocketClient() error { func runWebsocketClient() error {
if config.CloudToken == "" { cfg := config.LoadConfig()
if cfg.CloudToken == "" {
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
return fmt.Errorf("cloud token is not set") return fmt.Errorf("cloud token is not set")
} }
wsURL, err := url.Parse(config.CloudURL) wsURL, err := url.Parse(cfg.CloudURL)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse config.CloudURL: %w", err) return fmt.Errorf("failed to parse config.CloudURL: %w", err)
} }
@ -115,7 +121,7 @@ func runWebsocketClient() error {
} }
header := http.Header{} header := http.Header{}
header.Set("X-Device-ID", GetDeviceID()) header.Set("X-Device-ID", GetDeviceID())
header.Set("Authorization", "Bearer "+config.CloudToken) header.Set("Authorization", "Bearer "+cfg.CloudToken)
dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Minute) dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Minute)
defer cancelDial() defer cancelDial()
c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{
@ -125,7 +131,7 @@ func runWebsocketClient() error {
return err return err
} }
defer c.CloseNow() defer c.CloseNow()
logger.Infof("WS connected to %v", wsURL.String()) logging.Logger.Infof("WS connected to %v", wsURL.String())
runCtx, cancelRun := context.WithCancel(context.Background()) runCtx, cancelRun := context.WithCancel(context.Background())
defer cancelRun() defer cancelRun()
go func() { go func() {
@ -133,7 +139,7 @@ func runWebsocketClient() error {
time.Sleep(15 * time.Second) time.Sleep(15 * time.Second)
err := c.Ping(runCtx) err := c.Ping(runCtx)
if err != nil { if err != nil {
logger.Warnf("websocket ping error: %v", err) logging.Logger.Warnf("websocket ping error: %v", err)
cancelRun() cancelRun()
return return
} }
@ -151,19 +157,20 @@ func runWebsocketClient() error {
var req WebRTCSessionRequest var req WebRTCSessionRequest
err = json.Unmarshal(msg, &req) err = json.Unmarshal(msg, &req)
if err != nil { if err != nil {
logger.Warnf("unable to parse ws message: %v", string(msg)) logging.Logger.Warnf("unable to parse ws message: %v", string(msg))
continue continue
} }
err = handleSessionRequest(runCtx, c, req) err = handleSessionRequest(runCtx, c, req)
if err != nil { if err != nil {
logger.Infof("error starting new session: %v", err) logging.Logger.Infof("error starting new session: %v", err)
continue continue
} }
} }
} }
func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error {
cfg := config.LoadConfig()
oidcCtx, cancelOIDC := context.WithTimeout(ctx, time.Minute) oidcCtx, cancelOIDC := context.WithTimeout(ctx, time.Minute)
defer cancelOIDC() defer cancelOIDC()
provider, err := oidc.NewProvider(oidcCtx, "https://accounts.google.com") provider, err := oidc.NewProvider(oidcCtx, "https://accounts.google.com")
@ -183,11 +190,11 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
} }
googleIdentity := idToken.Audience[0] + ":" + idToken.Subject googleIdentity := idToken.Audience[0] + ":" + idToken.Subject
if config.GoogleIdentity != googleIdentity { if cfg.GoogleIdentity != googleIdentity {
return fmt.Errorf("google identity mismatch") return fmt.Errorf("google identity mismatch")
} }
session, err := newSession() session, err := NewSession()
if err != nil { if err != nil {
_ = wsjson.Write(context.Background(), c, gin.H{"error": err}) _ = wsjson.Write(context.Background(), c, gin.H{"error": err})
return err return err
@ -198,15 +205,15 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
_ = wsjson.Write(context.Background(), c, gin.H{"error": err}) _ = wsjson.Write(context.Background(), c, gin.H{"error": err})
return err return err
} }
if currentSession != nil { if CurrentSession != nil {
writeJSONRPCEvent("otherSessionConnected", nil, currentSession) WriteJSONRPCEvent("otherSessionConnected", nil, CurrentSession)
peerConn := currentSession.peerConnection peerConn := CurrentSession.PeerConnection
go func() { go func() {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
_ = peerConn.Close() _ = peerConn.Close()
}() }()
} }
currentSession = session CurrentSession = session
_ = wsjson.Write(context.Background(), c, gin.H{"sd": sd}) _ = wsjson.Write(context.Background(), c, gin.H{"sd": sd})
return nil return nil
} }
@ -226,24 +233,26 @@ type CloudState struct {
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
func rpcGetCloudState() CloudState { func RPCGetCloudState() CloudState {
cfg := config.LoadConfig()
return CloudState{ return CloudState{
Connected: config.CloudToken != "" && config.CloudURL != "", Connected: cfg.CloudToken != "" && cfg.CloudURL != "",
URL: config.CloudURL, URL: cfg.CloudURL,
} }
} }
func rpcDeregisterDevice() error { func RPCDeregisterDevice() error {
if config.CloudToken == "" || config.CloudURL == "" { cfg := config.LoadConfig()
if cfg.CloudToken == "" || cfg.CloudURL == "" {
return fmt.Errorf("cloud token or URL is not set") return fmt.Errorf("cloud token or URL is not set")
} }
req, err := http.NewRequest(http.MethodDelete, config.CloudURL+"/devices/"+GetDeviceID(), nil) req, err := http.NewRequest(http.MethodDelete, cfg.CloudURL+"/devices/"+GetDeviceID(), nil)
if err != nil { if err != nil {
return fmt.Errorf("failed to create deregister request: %w", err) return fmt.Errorf("failed to create deregister request: %w", err)
} }
req.Header.Set("Authorization", "Bearer "+config.CloudToken) req.Header.Set("Authorization", "Bearer "+cfg.CloudToken)
client := &http.Client{Timeout: 10 * time.Second} client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
@ -256,10 +265,10 @@ func rpcDeregisterDevice() error {
// 404 Not Found means the device is not in the database, which could be due to various reasons // 404 Not Found means the device is not in the database, which could be due to various reasons
// (e.g., wrong cloud token, already deregistered). Regardless of the reason, we can safely remove it. // (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) { if resp.StatusCode == http.StatusNotFound || (resp.StatusCode >= 200 && resp.StatusCode < 300) {
config.CloudToken = "" cfg.CloudToken = ""
config.CloudURL = "" cfg.CloudURL = ""
config.GoogleIdentity = "" cfg.GoogleIdentity = ""
if err := SaveConfig(); err != nil { if err := config.SaveConfig(cfg); err != nil {
return fmt.Errorf("failed to save configuration after deregistering: %w", err) return fmt.Errorf("failed to save configuration after deregistering: %w", err)
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"time" "time"
) )
var currentScreen = "ui_Boot_Screen" var currentScreen = "ui_Boot_Screen"
@ -34,23 +35,23 @@ func switchToScreenIfDifferent(screenName string) {
} }
func updateDisplay() { func updateDisplay() {
updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4) updateLabelIfChanged("ui_Home_Content_Ip", NetworkState.IPv4)
if usbState == "configured" { if UsbState == "configured" {
updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Connected") updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Connected")
_, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_DEFAULT"}) _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_DEFAULT"})
} else { } else {
updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Disconnected") updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Disconnected")
_, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_USER_2"}) _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_USER_2"})
} }
if lastVideoState.Ready { if LastVideoState.Ready {
updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Connected") updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Connected")
_, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_DEFAULT"}) _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_DEFAULT"})
} else { } else {
updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Disconnected") updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Disconnected")
_, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_USER_2"}) _, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_USER_2"})
} }
updateLabelIfChanged("ui_Home_Header_Cloud_Status_Label", fmt.Sprintf("%d active", actionSessions)) updateLabelIfChanged("ui_Home_Header_Cloud_Status_Label", fmt.Sprintf("%d active", ActionSessions))
if networkState.Up { if NetworkState.Up {
switchToScreenIfDifferent("ui_Home_Screen") switchToScreenIfDifferent("ui_Home_Screen")
} else { } else {
switchToScreenIfDifferent("ui_No_Network_Screen") switchToScreenIfDifferent("ui_No_Network_Screen")
@ -59,7 +60,7 @@ func updateDisplay() {
var displayInited = false var displayInited = false
func requestDisplayUpdate() { func RequestDisplayUpdate() {
if !displayInited { if !displayInited {
fmt.Println("display not inited, skipping updates") fmt.Println("display not inited, skipping updates")
return return
@ -73,7 +74,7 @@ func requestDisplayUpdate() {
func updateStaticContents() { func updateStaticContents() {
//contents that never change //contents that never change
updateLabelIfChanged("ui_Home_Content_Mac", networkState.MAC) updateLabelIfChanged("ui_Home_Content_Mac", NetworkState.MAC)
systemVersion, appVersion, err := GetLocalVersion() systemVersion, appVersion, err := GetLocalVersion()
if err == nil { if err == nil {
updateLabelIfChanged("ui_About_Content_Operating_System_Version_ContentLabel", systemVersion.String()) updateLabelIfChanged("ui_About_Content_Operating_System_Version_ContentLabel", systemVersion.String())
@ -85,12 +86,12 @@ func updateStaticContents() {
func init() { func init() {
go func() { go func() {
waitCtrlClientConnected() WaitCtrlClientConnected()
fmt.Println("setting initial display contents") fmt.Println("setting initial display contents")
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
updateStaticContents() updateStaticContents()
displayInited = true displayInited = true
fmt.Println("display inited") fmt.Println("display inited")
requestDisplayUpdate() RequestDisplayUpdate()
}() }()
} }

View File

@ -58,10 +58,10 @@ type DiskReadRequest struct {
End uint64 `json:"end"` End uint64 `json:"end"`
} }
var diskReadChan = make(chan []byte, 1) var DiskReadChan = make(chan []byte, 1)
func (f *WebRTCStreamFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { func (f *WebRTCStreamFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
buf, err := webRTCDiskReader.Read(ctx, off, int64(len(dest))) buf, err := WebRTCDiskReader.Read(ctx, off, int64(len(dest)))
if err != nil { if err != nil {
return nil, syscall.EIO return nil, syscall.EIO
} }

View File

@ -1,11 +1,16 @@
package kvm package kvm
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"strings"
"sync" "sync"
"time" "time"
"github.com/Masterminds/semver/v3"
"github.com/jetkvm/kvm/internal/logging"
) )
func extractSerialNumber() (string, error) { func extractSerialNumber() (string, error) {
@ -42,7 +47,7 @@ func GetDeviceID() string {
deviceIDOnce.Do(func() { deviceIDOnce.Do(func() {
serial, err := extractSerialNumber() serial, err := extractSerialNumber()
if err != nil { if err != nil {
logger.Warn("unknown serial number, the program likely not running on RV1106") logging.Logger.Warn("unknown serial number, the program likely not running on RV1106")
deviceID = "unknown_device_id" deviceID = "unknown_device_id"
} else { } else {
deviceID = serial deviceID = serial
@ -51,10 +56,10 @@ func GetDeviceID() string {
return deviceID return deviceID
} }
func runWatchdog() { func RunWatchdog(ctx context.Context) {
file, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0) file, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0)
if err != nil { if err != nil {
logger.Warnf("unable to open /dev/watchdog: %v, skipping watchdog reset", err) logging.Logger.Warnf("unable to open /dev/watchdog: %v, skipping watchdog reset", err)
return return
} }
defer file.Close() defer file.Close()
@ -65,15 +70,36 @@ func runWatchdog() {
case <-ticker.C: case <-ticker.C:
_, err = file.Write([]byte{0}) _, err = file.Write([]byte{0})
if err != nil { if err != nil {
logger.Errorf("error writing to /dev/watchdog, system may reboot: %v", err) logging.Logger.Errorf("error writing to /dev/watchdog, system may reboot: %v", err)
} }
case <-appCtx.Done(): case <-ctx.Done():
//disarm watchdog with magic value //disarm watchdog with magic value
_, err := file.Write([]byte("V")) _, err := file.Write([]byte("V"))
if err != nil { if err != nil {
logger.Errorf("failed to disarm watchdog, system may reboot: %v", err) logging.Logger.Errorf("failed to disarm watchdog, system may reboot: %v", err)
} }
return return
} }
} }
} }
var builtAppVersion = "0.1.0+dev"
func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Version, err error) {
appVersion, err = semver.NewVersion(builtAppVersion)
if err != nil {
return nil, nil, fmt.Errorf("invalid built-in app version: %w", err)
}
systemVersionBytes, err := os.ReadFile("/version")
if err != nil {
return nil, appVersion, fmt.Errorf("error reading system version: %w", err)
}
systemVersion, err = semver.NewVersion(strings.TrimSpace(string(systemVersionBytes)))
if err != nil {
return nil, appVersion, fmt.Errorf("invalid system version: %w", err)
}
return systemVersion, appVersion, nil
}

View File

@ -2,20 +2,22 @@ package kvm
import ( import (
"time" "time"
"github.com/jetkvm/kvm/internal/logging"
) )
var lastUserInput = time.Now() var lastUserInput = time.Now()
func resetUserInputTime() { func ResetUserInputTime() {
lastUserInput = time.Now() lastUserInput = time.Now()
} }
var jigglerEnabled = false var jigglerEnabled = false
func rpcSetJigglerState(enabled bool) { func RPCSetJigglerState(enabled bool) {
jigglerEnabled = enabled jigglerEnabled = enabled
} }
func rpcGetJigglerState() bool { func RPCGetJigglerState() bool {
return jigglerEnabled return jigglerEnabled
} }
@ -28,13 +30,13 @@ func runJiggler() {
if jigglerEnabled { if jigglerEnabled {
if time.Since(lastUserInput) > 20*time.Second { if time.Since(lastUserInput) > 20*time.Second {
//TODO: change to rel mouse //TODO: change to rel mouse
err := rpcAbsMouseReport(1, 1, 0) err := RPCAbsMouseReport(1, 1, 0)
if err != nil { if err != nil {
logger.Warnf("Failed to jiggle mouse: %v", err) logging.Logger.Warnf("Failed to jiggle mouse: %v", err)
} }
err = rpcAbsMouseReport(0, 0, 0) err = RPCAbsMouseReport(0, 0, 0)
if err != nil { if err != nil {
logger.Warnf("Failed to reset mouse position: %v", err) logging.Logger.Warnf("Failed to reset mouse position: %v", err)
} }
} }
} }

View File

@ -11,6 +11,9 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"github.com/jetkvm/kvm/internal/config"
"github.com/jetkvm/kvm/internal/logging"
"github.com/jetkvm/kvm/internal/wol"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
) )
@ -34,7 +37,7 @@ type JSONRPCEvent struct {
Params interface{} `json:"params,omitempty"` Params interface{} `json:"params,omitempty"`
} }
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) { func WriteJSONRPCResponse(response JSONRPCResponse, session *Session) {
responseBytes, err := json.Marshal(response) responseBytes, err := json.Marshal(response)
if err != nil { if err != nil {
log.Println("Error marshalling JSONRPC response:", err) log.Println("Error marshalling JSONRPC response:", err)
@ -47,7 +50,7 @@ func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
} }
} }
func writeJSONRPCEvent(event string, params interface{}, session *Session) { func WriteJSONRPCEvent(event string, params interface{}, session *Session) {
request := JSONRPCEvent{ request := JSONRPCEvent{
JSONRPC: "2.0", JSONRPC: "2.0",
Method: event, Method: event,
@ -69,7 +72,7 @@ func writeJSONRPCEvent(event string, params interface{}, session *Session) {
} }
} }
func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { func OnRPCMessage(message webrtc.DataChannelMessage, session *Session) {
var request JSONRPCRequest var request JSONRPCRequest
err := json.Unmarshal(message.Data, &request) err := json.Unmarshal(message.Data, &request)
if err != nil { if err != nil {
@ -81,7 +84,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
}, },
ID: 0, ID: 0,
} }
writeJSONRPCResponse(errorResponse, session) WriteJSONRPCResponse(errorResponse, session)
return return
} }
@ -96,7 +99,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
}, },
ID: request.ID, ID: request.ID,
} }
writeJSONRPCResponse(errorResponse, session) WriteJSONRPCResponse(errorResponse, session)
return return
} }
@ -111,7 +114,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
}, },
ID: request.ID, ID: request.ID,
} }
writeJSONRPCResponse(errorResponse, session) WriteJSONRPCResponse(errorResponse, session)
return return
} }
@ -120,7 +123,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
Result: result, Result: result,
ID: request.ID, ID: request.ID,
} }
writeJSONRPCResponse(response, session) WriteJSONRPCResponse(response, session)
} }
func rpcPing() (string, error) { func rpcPing() (string, error) {
@ -149,13 +152,15 @@ func rpcSetStreamQualityFactor(factor float64) error {
} }
func rpcGetAutoUpdateState() (bool, error) { func rpcGetAutoUpdateState() (bool, error) {
return config.AutoUpdateEnabled, nil cfg := config.LoadConfig()
return cfg.AutoUpdateEnabled, nil
} }
func rpcSetAutoUpdateState(enabled bool) (bool, error) { func rpcSetAutoUpdateState(enabled bool) (bool, error) {
config.AutoUpdateEnabled = enabled cfg := config.LoadConfig()
if err := SaveConfig(); err != nil { cfg.AutoUpdateEnabled = enabled
return config.AutoUpdateEnabled, fmt.Errorf("failed to save config: %w", err) if err := config.SaveConfig(cfg); err != nil {
return cfg.AutoUpdateEnabled, fmt.Errorf("failed to save config: %w", err)
} }
return enabled, nil return enabled, nil
} }
@ -187,19 +192,22 @@ func rpcSetEDID(edid string) error {
} }
func rpcGetDevChannelState() (bool, error) { func rpcGetDevChannelState() (bool, error) {
return config.IncludePreRelease, nil cfg := config.LoadConfig()
return cfg.IncludePreRelease, nil
} }
func rpcSetDevChannelState(enabled bool) error { func rpcSetDevChannelState(enabled bool) error {
config.IncludePreRelease = enabled cfg := config.LoadConfig()
if err := SaveConfig(); err != nil { cfg.IncludePreRelease = enabled
if err := config.SaveConfig(cfg); err != nil {
return fmt.Errorf("failed to save config: %w", err) return fmt.Errorf("failed to save config: %w", err)
} }
return nil return nil
} }
func rpcGetUpdateStatus() (*UpdateStatus, error) { func rpcGetUpdateStatus() (*UpdateStatus, error) {
includePreRelease := config.IncludePreRelease cfg := config.LoadConfig()
includePreRelease := cfg.IncludePreRelease
updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease) updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease)
if err != nil { if err != nil {
return nil, fmt.Errorf("error checking for updates: %w", err) return nil, fmt.Errorf("error checking for updates: %w", err)
@ -209,11 +217,12 @@ func rpcGetUpdateStatus() (*UpdateStatus, error) {
} }
func rpcTryUpdate() error { func rpcTryUpdate() error {
includePreRelease := config.IncludePreRelease cfg := config.LoadConfig()
includePreRelease := cfg.IncludePreRelease
go func() { go func() {
err := TryUpdate(context.Background(), GetDeviceID(), includePreRelease) err := TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
if err != nil { if err != nil {
logger.Warnf("failed to try update: %v", err) logging.Logger.Warnf("failed to try update: %v", err)
} }
}() }()
return nil return nil
@ -258,7 +267,7 @@ func rpcSetDevModeState(enabled bool) error {
return fmt.Errorf("failed to create devmode file: %w", err) return fmt.Errorf("failed to create devmode file: %w", err)
} }
} else { } else {
logger.Debug("dev mode already enabled") logging.Logger.Debug("dev mode already enabled")
return nil return nil
} }
} else { } else {
@ -267,7 +276,7 @@ func rpcSetDevModeState(enabled bool) error {
return fmt.Errorf("failed to remove devmode file: %w", err) return fmt.Errorf("failed to remove devmode file: %w", err)
} }
} else if os.IsNotExist(err) { } else if os.IsNotExist(err) {
logger.Debug("dev mode already disabled") logging.Logger.Debug("dev mode already disabled")
return nil return nil
} else { } else {
return fmt.Errorf("error checking dev mode file: %w", err) return fmt.Errorf("error checking dev mode file: %w", err)
@ -277,7 +286,7 @@ func rpcSetDevModeState(enabled bool) error {
cmd := exec.Command("dropbear.sh") cmd := exec.Command("dropbear.sh")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
logger.Warnf("Failed to start/stop SSH: %v, %v", err, output) logging.Logger.Warnf("Failed to start/stop SSH: %v, %v", err, output)
return fmt.Errorf("failed to start/stop SSH, you may need to reboot for changes to take effect") return fmt.Errorf("failed to start/stop SSH, you may need to reboot for changes to take effect")
} }
@ -429,7 +438,7 @@ func rpcSetMassStorageMode(mode string) (string, error) {
log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode)
err := setMassStorageMode(cdrom) err := SetMassStorageMode(cdrom)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to set mass storage mode: %w", err) return "", fmt.Errorf("failed to set mass storage mode: %w", err)
} }
@ -441,7 +450,7 @@ func rpcSetMassStorageMode(mode string) (string, error) {
} }
func rpcGetMassStorageMode() (string, error) { func rpcGetMassStorageMode() (string, error) {
cdrom, err := getMassStorageMode() cdrom, err := GetMassStorageMode()
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get mass storage mode: %w", err) return "", fmt.Errorf("failed to get mass storage mode: %w", err)
} }
@ -457,7 +466,7 @@ func rpcIsUpdatePending() (bool, error) {
return IsUpdatePending(), nil return IsUpdatePending(), nil
} }
var udcFilePath = filepath.Join("/sys/bus/platform/drivers/dwc3", udc) var udcFilePath = filepath.Join("/sys/bus/platform/drivers/dwc3", Udc)
func rpcGetUsbEmulationState() (bool, error) { func rpcGetUsbEmulationState() (bool, error) {
_, err := os.Stat(udcFilePath) _, err := os.Stat(udcFilePath)
@ -472,34 +481,32 @@ func rpcGetUsbEmulationState() (bool, error) {
func rpcSetUsbEmulationState(enabled bool) error { func rpcSetUsbEmulationState(enabled bool) error {
if enabled { if enabled {
return os.WriteFile("/sys/bus/platform/drivers/dwc3/bind", []byte(udc), 0644) return os.WriteFile("/sys/bus/platform/drivers/dwc3/bind", []byte(Udc), 0644)
} else { } else {
return os.WriteFile("/sys/bus/platform/drivers/dwc3/unbind", []byte(udc), 0644) return os.WriteFile("/sys/bus/platform/drivers/dwc3/unbind", []byte(Udc), 0644)
} }
} }
func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { func rpcGetWakeOnLanDevices() ([]config.WakeOnLanDevice, error) {
LoadConfig() cfg := config.LoadConfig()
if config.WakeOnLanDevices == nil { if cfg.WakeOnLanDevices == nil {
return []WakeOnLanDevice{}, nil return []config.WakeOnLanDevice{}, nil
} }
return config.WakeOnLanDevices, nil return cfg.WakeOnLanDevices, nil
} }
type SetWakeOnLanDevicesParams struct { type SetWakeOnLanDevicesParams struct {
Devices []WakeOnLanDevice `json:"devices"` Devices []config.WakeOnLanDevice `json:"devices"`
} }
func rpcSetWakeOnLanDevices(params SetWakeOnLanDevicesParams) error { func rpcSetWakeOnLanDevices(params SetWakeOnLanDevicesParams) error {
LoadConfig() cfg := config.LoadConfig()
config.WakeOnLanDevices = params.Devices cfg.WakeOnLanDevices = params.Devices
return SaveConfig() return config.SaveConfig(cfg)
} }
func rpcResetConfig() error { func rpcResetConfig() error {
LoadConfig() if err := config.SaveConfig(&config.Config{}); err != nil {
config = defaultConfig
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to reset config: %w", err) return fmt.Errorf("failed to reset config: %w", err)
} }
@ -511,18 +518,18 @@ func rpcResetConfig() error {
var rpcHandlers = map[string]RPCHandler{ var rpcHandlers = map[string]RPCHandler{
"ping": {Func: rpcPing}, "ping": {Func: rpcPing},
"getDeviceID": {Func: rpcGetDeviceID}, "getDeviceID": {Func: rpcGetDeviceID},
"deregisterDevice": {Func: rpcDeregisterDevice}, "deregisterDevice": {Func: RPCDeregisterDevice},
"getCloudState": {Func: rpcGetCloudState}, "getCloudState": {Func: RPCGetCloudState},
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, "keyboardReport": {Func: RPCKeyboardReport, Params: []string{"modifier", "keys"}},
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, "absMouseReport": {Func: RPCAbsMouseReport, Params: []string{"x", "y", "buttons"}},
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, "wheelReport": {Func: RPCWheelReport, Params: []string{"wheelY"}},
"getVideoState": {Func: rpcGetVideoState}, "getVideoState": {Func: rpcGetVideoState},
"getUSBState": {Func: rpcGetUSBState}, "getUSBState": {Func: RPCGetUSBState},
"unmountImage": {Func: rpcUnmountImage}, "unmountImage": {Func: RPCUnmountImage},
"rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, "rpcMountBuiltInImage": {Func: RPCMountBuiltInImage, Params: []string{"filename"}},
"setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, "setJigglerState": {Func: RPCSetJigglerState, Params: []string{"enabled"}},
"getJigglerState": {Func: rpcGetJigglerState}, "getJigglerState": {Func: RPCGetJigglerState},
"sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, "sendWOLMagicPacket": {Func: wol.RPCSendWolMagicPacket, Params: []string{"macAddress"}},
"getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor},
"setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},
"getAutoUpdateState": {Func: rpcGetAutoUpdateState}, "getAutoUpdateState": {Func: rpcGetAutoUpdateState},
@ -542,15 +549,15 @@ var rpcHandlers = map[string]RPCHandler{
"isUpdatePending": {Func: rpcIsUpdatePending}, "isUpdatePending": {Func: rpcIsUpdatePending},
"getUsbEmulationState": {Func: rpcGetUsbEmulationState}, "getUsbEmulationState": {Func: rpcGetUsbEmulationState},
"setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}},
"checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, "checkMountUrl": {Func: RPCCheckMountUrl, Params: []string{"url"}},
"getVirtualMediaState": {Func: rpcGetVirtualMediaState}, "getVirtualMediaState": {Func: RPCGetVirtualMediaState},
"getStorageSpace": {Func: rpcGetStorageSpace}, "getStorageSpace": {Func: RPCGetStorageSpace},
"mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, "mountWithHTTP": {Func: RPCMountWithHTTP, Params: []string{"url", "mode"}},
"mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, "mountWithWebRTC": {Func: RPCMountWithWebRTC, Params: []string{"filename", "size", "mode"}},
"mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, "mountWithStorage": {Func: RPCMountWithStorage, Params: []string{"filename", "mode"}},
"listStorageFiles": {Func: rpcListStorageFiles}, "listStorageFiles": {Func: RPCListStorageFiles},
"deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, "deleteStorageFile": {Func: RPCDeleteStorageFile, Params: []string{"filename"}},
"startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, "startStorageFileUpload": {Func: RPCStartStorageFileUpload, Params: []string{"filename", "size"}},
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
"resetConfig": {Func: rpcResetConfig}, "resetConfig": {Func: rpcResetConfig},

View File

@ -2,10 +2,10 @@ package kvm
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"kvm/resource"
"log" "log"
"net" "net"
"os" "os"
@ -13,6 +13,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/jetkvm/kvm/resource"
"github.com/jetkvm/kvm/internal/logging"
"github.com/pion/webrtc/v4/pkg/media" "github.com/pion/webrtc/v4/pkg/media"
) )
@ -95,7 +98,7 @@ var nativeVideoSocketListener net.Listener
var ctrlClientConnected = make(chan struct{}) var ctrlClientConnected = make(chan struct{})
func waitCtrlClientConnected() { func WaitCtrlClientConnected() {
<-ctrlClientConnected <-ctrlClientConnected
} }
@ -118,11 +121,11 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC
conn, err := listener.Accept() conn, err := listener.Accept()
listener.Close() listener.Close()
if err != nil { if err != nil {
logger.Errorf("failed to accept sock: %v", err) logging.Logger.Errorf("failed to accept sock: %v", err)
} }
if isCtrl { if isCtrl {
close(ctrlClientConnected) close(ctrlClientConnected)
logger.Debug("first native ctrl socket client connected") logging.Logger.Debug("first native ctrl socket client connected")
} }
handleClient(conn) handleClient(conn)
}() }()
@ -132,20 +135,20 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC
func StartNativeCtrlSocketServer() { func StartNativeCtrlSocketServer() {
nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true) nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true)
logger.Debug("native app ctrl sock started") logging.Logger.Debug("native app ctrl sock started")
} }
func StartNativeVideoSocketServer() { func StartNativeVideoSocketServer() {
nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false) nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false)
logger.Debug("native app video sock started") logging.Logger.Debug("native app video sock started")
} }
func handleCtrlClient(conn net.Conn) { func handleCtrlClient(conn net.Conn) {
defer conn.Close() defer conn.Close()
logger.Debug("native socket client connected") logging.Logger.Debug("native socket client connected")
if ctrlSocketConn != nil { if ctrlSocketConn != nil {
logger.Debugf("closing existing native socket connection") logging.Logger.Debugf("closing existing native socket connection")
ctrlSocketConn.Close() ctrlSocketConn.Close()
} }
@ -155,15 +158,15 @@ func handleCtrlClient(conn net.Conn) {
for { for {
n, err := conn.Read(readBuf) n, err := conn.Read(readBuf)
if err != nil { if err != nil {
logger.Errorf("error reading from ctrl sock: %v", err) logging.Logger.Errorf("error reading from ctrl sock: %v", err)
break break
} }
readMsg := string(readBuf[:n]) readMsg := string(readBuf[:n])
logger.Tracef("ctrl sock msg: %v", readMsg) logging.Logger.Tracef("ctrl sock msg: %v", readMsg)
ctrlResp := CtrlResponse{} ctrlResp := CtrlResponse{}
err = json.Unmarshal([]byte(readMsg), &ctrlResp) err = json.Unmarshal([]byte(readMsg), &ctrlResp)
if err != nil { if err != nil {
logger.Warnf("error parsing ctrl sock msg: %v", err) logging.Logger.Warnf("error parsing ctrl sock msg: %v", err)
continue continue
} }
if ctrlResp.Seq != 0 { if ctrlResp.Seq != 0 {
@ -178,7 +181,7 @@ func handleCtrlClient(conn net.Conn) {
} }
} }
logger.Debug("ctrl sock disconnected") logging.Logger.Debug("ctrl sock disconnected")
} }
func handleVideoClient(conn net.Conn) { func handleVideoClient(conn net.Conn) {
@ -186,7 +189,7 @@ func handleVideoClient(conn net.Conn) {
log.Printf("Native video socket client connected: %v", conn.RemoteAddr()) log.Printf("Native video socket client connected: %v", conn.RemoteAddr())
inboundPacket := make([]byte, maxFrameSize) inboundPacket := make([]byte, MaxFrameSize)
lastFrame := time.Now() lastFrame := time.Now()
for { for {
n, err := conn.Read(inboundPacket) n, err := conn.Read(inboundPacket)
@ -198,8 +201,8 @@ func handleVideoClient(conn net.Conn) {
sinceLastFrame := now.Sub(lastFrame) sinceLastFrame := now.Sub(lastFrame)
lastFrame = now lastFrame = now
//fmt.Println("Video packet received", n, sinceLastFrame) //fmt.Println("Video packet received", n, sinceLastFrame)
if currentSession != nil { if CurrentSession != nil {
err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame}) err := CurrentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame})
if err != nil { if err != nil {
log.Println("Error writing sample", err) log.Println("Error writing sample", err)
} }
@ -207,7 +210,7 @@ func handleVideoClient(conn net.Conn) {
} }
} }
func ExtractAndRunNativeBin() error { func ExtractAndRunNativeBin(ctx context.Context) error {
binaryPath := "/userdata/jetkvm/bin/jetkvm_native" binaryPath := "/userdata/jetkvm/bin/jetkvm_native"
if err := ensureBinaryUpdated(binaryPath); err != nil { if err := ensureBinaryUpdated(binaryPath); err != nil {
return fmt.Errorf("failed to extract binary: %w", err) return fmt.Errorf("failed to extract binary: %w", err)
@ -231,11 +234,11 @@ func ExtractAndRunNativeBin() error {
//TODO: add auto restart //TODO: add auto restart
go func() { go func() {
<-appCtx.Done() <-ctx.Done()
logger.Infof("killing process PID: %d", cmd.Process.Pid) logging.Logger.Infof("killing process PID: %d", cmd.Process.Pid)
err := cmd.Process.Kill() err := cmd.Process.Kill()
if err != nil { if err != nil {
logger.Errorf("failed to kill process: %v", err) logging.Logger.Errorf("failed to kill process: %v", err)
return return
} }
}() }()
@ -247,13 +250,13 @@ func ExtractAndRunNativeBin() error {
func shouldOverwrite(destPath string, srcHash []byte) bool { func shouldOverwrite(destPath string, srcHash []byte) bool {
if srcHash == nil { if srcHash == nil {
logger.Debug("error reading embedded jetkvm_native.sha256, doing overwriting") logging.Logger.Debug("error reading embedded jetkvm_native.sha256, doing overwriting")
return true return true
} }
dstHash, err := os.ReadFile(destPath + ".sha256") dstHash, err := os.ReadFile(destPath + ".sha256")
if err != nil { if err != nil {
logger.Debug("error reading existing jetkvm_native.sha256, doing overwriting") logging.Logger.Debug("error reading existing jetkvm_native.sha256, doing overwriting")
return true return true
} }
@ -269,13 +272,13 @@ func ensureBinaryUpdated(destPath string) error {
srcHash, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256") srcHash, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256")
if err != nil { if err != nil {
logger.Debug("error reading embedded jetkvm_native.sha256, proceeding with update") logging.Logger.Debug("error reading embedded jetkvm_native.sha256, proceeding with update")
srcHash = nil srcHash = nil
} }
_, err = os.Stat(destPath) _, err = os.Stat(destPath)
if shouldOverwrite(destPath, srcHash) || err != nil { if shouldOverwrite(destPath, srcHash) || err != nil {
logger.Info("writing jetkvm_native") logging.Logger.Info("writing jetkvm_native")
_ = os.Remove(destPath) _ = os.Remove(destPath)
destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755) destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755)
if err != nil { if err != nil {
@ -292,7 +295,7 @@ func ensureBinaryUpdated(destPath string) error {
return err return err
} }
} }
logger.Info("jetkvm_native updated") logging.Logger.Info("jetkvm_native updated")
} }
return nil return nil

View File

@ -2,17 +2,17 @@ package kvm
import ( import (
"fmt" "fmt"
"github.com/pion/mdns/v2"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"net" "net"
"time" "time"
"github.com/pion/mdns/v2"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl" "github.com/vishvananda/netlink/nl"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
) )
var networkState struct { var NetworkState struct {
Up bool Up bool
IPv4 string IPv4 string
IPv6 string IPv6 string
@ -55,10 +55,10 @@ func checkNetworkState() {
} }
} }
if newState != networkState { if newState != NetworkState {
networkState = newState NetworkState = newState
fmt.Println("network state changed") fmt.Println("network state changed")
requestDisplayUpdate() RequestDisplayUpdate()
} }
} }
@ -103,7 +103,7 @@ func init() {
} }
go func() { go func() {
waitCtrlClientConnected() WaitCtrlClientConnected()
checkNetworkState() checkNetworkState()
ticker := time.NewTicker(1 * time.Second) ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() defer ticker.Stop()

View File

@ -13,10 +13,10 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"strings"
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/jetkvm/kvm/internal/logging"
) )
type UpdateMetadata struct { type UpdateMetadata struct {
@ -43,27 +43,6 @@ type UpdateStatus struct {
const UpdateMetadataUrl = "https://api.jetkvm.com/releases" const UpdateMetadataUrl = "https://api.jetkvm.com/releases"
var builtAppVersion = "0.1.0+dev"
func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Version, err error) {
appVersion, err = semver.NewVersion(builtAppVersion)
if err != nil {
return nil, nil, fmt.Errorf("invalid built-in app version: %w", err)
}
systemVersionBytes, err := os.ReadFile("/version")
if err != nil {
return nil, appVersion, fmt.Errorf("error reading system version: %w", err)
}
systemVersion, err = semver.NewVersion(strings.TrimSpace(string(systemVersionBytes)))
if err != nil {
return nil, appVersion, fmt.Errorf("invalid system version: %w", err)
}
return systemVersion, appVersion, nil
}
func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateMetadata, error) { func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateMetadata, error) {
metadata := &UpdateMetadata{} metadata := &UpdateMetadata{}
@ -158,7 +137,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
progress := float32(written) / float32(totalSize) progress := float32(written) / float32(totalSize)
if progress-*downloadProgress >= 0.01 { if progress-*downloadProgress >= 0.01 {
*downloadProgress = progress *downloadProgress = progress
triggerOTAStateUpdate() TriggerOTAStateUpdate()
} }
} }
if er != nil { if er != nil {
@ -218,7 +197,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32) error
progress := float32(verified) / float32(totalSize) progress := float32(verified) / float32(totalSize)
if progress-*verifyProgress >= 0.01 { if progress-*verifyProgress >= 0.01 {
*verifyProgress = progress *verifyProgress = progress
triggerOTAStateUpdate() TriggerOTAStateUpdate()
} }
} }
if er != nil { if er != nil {
@ -269,13 +248,13 @@ type OTAState struct {
var otaState = OTAState{} var otaState = OTAState{}
func triggerOTAStateUpdate() { func TriggerOTAStateUpdate() {
go func() { go func() {
if currentSession == nil { if CurrentSession == nil {
log.Println("No active RPC session, skipping update state update") log.Println("No active RPC session, skipping update state update")
return return
} }
writeJSONRPCEvent("otaState", otaState, currentSession) WriteJSONRPCEvent("otaState", otaState, CurrentSession)
}() }()
} }
@ -288,11 +267,11 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState = OTAState{ otaState = OTAState{
Updating: true, Updating: true,
} }
triggerOTAStateUpdate() TriggerOTAStateUpdate()
defer func() { defer func() {
otaState.Updating = false otaState.Updating = false
triggerOTAStateUpdate() TriggerOTAStateUpdate()
}() }()
updateStatus, err := GetUpdateStatus(ctx, deviceId, includePreRelease) updateStatus, err := GetUpdateStatus(ctx, deviceId, includePreRelease)
@ -305,7 +284,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.MetadataFetchedAt = &now otaState.MetadataFetchedAt = &now
otaState.AppUpdatePending = updateStatus.AppUpdateAvailable otaState.AppUpdatePending = updateStatus.AppUpdateAvailable
otaState.SystemUpdatePending = updateStatus.SystemUpdateAvailable otaState.SystemUpdatePending = updateStatus.SystemUpdateAvailable
triggerOTAStateUpdate() TriggerOTAStateUpdate()
local := updateStatus.Local local := updateStatus.Local
remote := updateStatus.Remote remote := updateStatus.Remote
@ -320,18 +299,18 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress) err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress)
if err != nil { if err != nil {
otaState.Error = fmt.Sprintf("Error downloading app update: %v", err) otaState.Error = fmt.Sprintf("Error downloading app update: %v", err)
triggerOTAStateUpdate() TriggerOTAStateUpdate()
return err return err
} }
downloadFinished := time.Now() downloadFinished := time.Now()
otaState.AppDownloadFinishedAt = &downloadFinished otaState.AppDownloadFinishedAt = &downloadFinished
otaState.AppDownloadProgress = 1 otaState.AppDownloadProgress = 1
triggerOTAStateUpdate() TriggerOTAStateUpdate()
err = verifyFile("/userdata/jetkvm/jetkvm_app.update", remote.AppHash, &otaState.AppVerificationProgress) err = verifyFile("/userdata/jetkvm/jetkvm_app.update", remote.AppHash, &otaState.AppVerificationProgress)
if err != nil { if err != nil {
otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err) otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err)
triggerOTAStateUpdate() TriggerOTAStateUpdate()
return err return err
} }
verifyFinished := time.Now() verifyFinished := time.Now()
@ -339,7 +318,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.AppVerificationProgress = 1 otaState.AppVerificationProgress = 1
otaState.AppUpdatedAt = &verifyFinished otaState.AppUpdatedAt = &verifyFinished
otaState.AppUpdateProgress = 1 otaState.AppUpdateProgress = 1
triggerOTAStateUpdate() TriggerOTAStateUpdate()
fmt.Println("App update downloaded") fmt.Println("App update downloaded")
rebootNeeded = true rebootNeeded = true
@ -352,25 +331,25 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress) err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress)
if err != nil { if err != nil {
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
triggerOTAStateUpdate() TriggerOTAStateUpdate()
return err return err
} }
downloadFinished := time.Now() downloadFinished := time.Now()
otaState.SystemDownloadFinishedAt = &downloadFinished otaState.SystemDownloadFinishedAt = &downloadFinished
otaState.SystemDownloadProgress = 1 otaState.SystemDownloadProgress = 1
triggerOTAStateUpdate() TriggerOTAStateUpdate()
err = verifyFile("/userdata/jetkvm/update_system.tar", remote.SystemHash, &otaState.SystemVerificationProgress) err = verifyFile("/userdata/jetkvm/update_system.tar", remote.SystemHash, &otaState.SystemVerificationProgress)
if err != nil { if err != nil {
otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err) otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err)
triggerOTAStateUpdate() TriggerOTAStateUpdate()
return err return err
} }
fmt.Println("System update downloaded") fmt.Println("System update downloaded")
verifyFinished := time.Now() verifyFinished := time.Now()
otaState.SystemVerifiedAt = &verifyFinished otaState.SystemVerifiedAt = &verifyFinished
otaState.SystemVerificationProgress = 1 otaState.SystemVerificationProgress = 1
triggerOTAStateUpdate() TriggerOTAStateUpdate()
cmd := exec.Command("rk_ota", "--misc=update", "--tar_path=/userdata/jetkvm/update_system.tar", "--save_dir=/userdata/jetkvm/ota_save", "--partition=all") cmd := exec.Command("rk_ota", "--misc=update", "--tar_path=/userdata/jetkvm/update_system.tar", "--save_dir=/userdata/jetkvm/ota_save", "--partition=all")
var b bytes.Buffer var b bytes.Buffer
@ -398,7 +377,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
if otaState.SystemUpdateProgress > 0.99 { if otaState.SystemUpdateProgress > 0.99 {
otaState.SystemUpdateProgress = 0.99 otaState.SystemUpdateProgress = 0.99
} }
triggerOTAStateUpdate() TriggerOTAStateUpdate()
case <-ctx.Done(): case <-ctx.Done():
return return
} }
@ -416,7 +395,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
fmt.Printf("rk_ota success, output: %s\n", output) fmt.Printf("rk_ota success, output: %s\n", output)
otaState.SystemUpdateProgress = 1 otaState.SystemUpdateProgress = 1
otaState.SystemUpdatedAt = &verifyFinished otaState.SystemUpdatedAt = &verifyFinished
triggerOTAStateUpdate() TriggerOTAStateUpdate()
rebootNeeded = true rebootNeeded = true
} else { } else {
fmt.Println("System is up to date") fmt.Println("System is up to date")
@ -495,9 +474,9 @@ func IsUpdatePending() bool {
} }
// make sure our current a/b partition is set as default // make sure our current a/b partition is set as default
func confirmCurrentSystem() { func ConfirmCurrentSystem() {
output, err := exec.Command("rk_ota", "--misc=now").CombinedOutput() output, err := exec.Command("rk_ota", "--misc=now").CombinedOutput()
if err != nil { if err != nil {
logger.Warnf("failed to set current partition in A/B setup: %s", string(output)) logging.Logger.Warnf("failed to set current partition in A/B setup: %s", string(output))
} }
} }

View File

@ -4,29 +4,31 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/jetkvm/kvm/internal/logging"
) )
type RemoteImageReader interface { type RemoteImageReader interface {
Read(ctx context.Context, offset int64, size int64) ([]byte, error) Read(ctx context.Context, offset int64, size int64) ([]byte, error)
} }
type WebRTCDiskReader struct { type WebRTCDiskReaderStruct struct {
} }
var webRTCDiskReader WebRTCDiskReader var WebRTCDiskReader WebRTCDiskReaderStruct
func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) ([]byte, error) { func (w *WebRTCDiskReaderStruct) Read(ctx context.Context, offset int64, size int64) ([]byte, error) {
virtualMediaStateMutex.RLock() VirtualMediaStateMutex.RLock()
if currentVirtualMediaState == nil { if CurrentVirtualMediaState == nil {
virtualMediaStateMutex.RUnlock() VirtualMediaStateMutex.RUnlock()
return nil, errors.New("image not mounted") return nil, errors.New("image not mounted")
} }
if currentVirtualMediaState.Source != WebRTC { if CurrentVirtualMediaState.Source != WebRTC {
virtualMediaStateMutex.RUnlock() VirtualMediaStateMutex.RUnlock()
return nil, errors.New("image not mounted from webrtc") return nil, errors.New("image not mounted from webrtc")
} }
mountedImageSize := currentVirtualMediaState.Size mountedImageSize := CurrentVirtualMediaState.Size
virtualMediaStateMutex.RUnlock() VirtualMediaStateMutex.RUnlock()
end := offset + size end := offset + size
if end > mountedImageSize { if end > mountedImageSize {
end = mountedImageSize end = mountedImageSize
@ -40,19 +42,19 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) (
return nil, err return nil, err
} }
if currentSession == nil || currentSession.DiskChannel == nil { if CurrentSession == nil || CurrentSession.DiskChannel == nil {
return nil, errors.New("not active session") return nil, errors.New("not active session")
} }
logger.Debugf("reading from webrtc %v", string(jsonBytes)) logging.Logger.Debugf("reading from webrtc %v", string(jsonBytes))
err = currentSession.DiskChannel.SendText(string(jsonBytes)) err = CurrentSession.DiskChannel.SendText(string(jsonBytes))
if err != nil { if err != nil {
return nil, err return nil, err
} }
buf := make([]byte, 0) buf := make([]byte, 0)
for { for {
select { select {
case data := <-diskReadChan: case data := <-DiskReadChan:
buf = data[16:] buf = data[16:]
case <-ctx.Done(): case <-ctx.Done():
return nil, context.Canceled return nil, context.Canceled

View File

@ -7,6 +7,7 @@ import (
"os/exec" "os/exec"
"github.com/creack/pty" "github.com/creack/pty"
"github.com/jetkvm/kvm/internal/logging"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
) )
@ -15,7 +16,7 @@ type TerminalSize struct {
Cols int `json:"cols"` Cols int `json:"cols"`
} }
func handleTerminalChannel(d *webrtc.DataChannel) { func HandleTerminalChannel(d *webrtc.DataChannel) {
var ptmx *os.File var ptmx *os.File
var cmd *exec.Cmd var cmd *exec.Cmd
d.OnOpen(func() { d.OnOpen(func() {
@ -23,7 +24,7 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
var err error var err error
ptmx, err = pty.Start(cmd) ptmx, err = pty.Start(cmd)
if err != nil { if err != nil {
logger.Errorf("Failed to start pty: %v", err) logging.Logger.Errorf("Failed to start pty: %v", err)
d.Close() d.Close()
return return
} }
@ -34,13 +35,13 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
n, err := ptmx.Read(buf) n, err := ptmx.Read(buf)
if err != nil { if err != nil {
if err != io.EOF { if err != io.EOF {
logger.Errorf("Failed to read from pty: %v", err) logging.Logger.Errorf("Failed to read from pty: %v", err)
} }
break break
} }
err = d.Send(buf[:n]) err = d.Send(buf[:n])
if err != nil { if err != nil {
logger.Errorf("Failed to send pty output: %v", err) logging.Logger.Errorf("Failed to send pty output: %v", err)
break break
} }
} }
@ -61,11 +62,11 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
}) })
return return
} }
logger.Errorf("Failed to parse terminal size: %v", err) logging.Logger.Errorf("Failed to parse terminal size: %v", err)
} }
_, err := ptmx.Write(msg.Data) _, err := ptmx.Write(msg.Data)
if err != nil { if err != nil {
logger.Errorf("Failed to write to pty: %v", err) logging.Logger.Errorf("Failed to write to pty: %v", err)
} }
}) })

View File

@ -12,6 +12,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/jetkvm/kvm/internal/logging"
gadget "github.com/openstadia/go-usb-gadget" gadget "github.com/openstadia/go-usb-gadget"
) )
@ -37,22 +38,22 @@ func init() {
_ = os.MkdirAll(imagesFolder, 0755) _ = os.MkdirAll(imagesFolder, 0755)
udcs := gadget.GetUdcs() udcs := gadget.GetUdcs()
if len(udcs) < 1 { if len(udcs) < 1 {
usbLogger.Error("no udc found, skipping USB stack init") logging.UsbLogger.Error("no udc found, skipping USB stack init")
return return
} }
udc = udcs[0] Udc = udcs[0]
_, err := os.Stat(kvmGadgetPath) _, err := os.Stat(kvmGadgetPath)
if err == nil { if err == nil {
logger.Info("usb gadget already exists, skipping usb gadget initialization") logging.Logger.Info("usb gadget already exists, skipping usb gadget initialization")
return return
} }
err = mountConfigFS() err = mountConfigFS()
if err != nil { if err != nil {
logger.Errorf("failed to mount configfs: %v, usb stack might not function properly", err) logging.Logger.Errorf("failed to mount configfs: %v, usb stack might not function properly", err)
} }
err = writeGadgetConfig() err = writeGadgetConfig()
if err != nil { if err != nil {
logger.Errorf("failed to start gadget: %v", err) logging.Logger.Errorf("failed to start gadget: %v", err)
} }
//TODO: read hid reports(capslock, numlock, etc) from keyboardHidFile //TODO: read hid reports(capslock, numlock, etc) from keyboardHidFile
@ -207,7 +208,7 @@ func writeGadgetConfig() error {
return err return err
} }
err = os.WriteFile(path.Join(kvmGadgetPath, "UDC"), []byte(udc), 0644) err = os.WriteFile(path.Join(kvmGadgetPath, "UDC"), []byte(Udc), 0644)
if err != nil { if err != nil {
return err return err
} }
@ -216,11 +217,11 @@ func writeGadgetConfig() error {
} }
func rebindUsb() error { func rebindUsb() error {
err := os.WriteFile("/sys/bus/platform/drivers/dwc3/unbind", []byte(udc), 0644) err := os.WriteFile("/sys/bus/platform/drivers/dwc3/unbind", []byte(Udc), 0644)
if err != nil { if err != nil {
return err return err
} }
err = os.WriteFile("/sys/bus/platform/drivers/dwc3/bind", []byte(udc), 0644) err = os.WriteFile("/sys/bus/platform/drivers/dwc3/bind", []byte(Udc), 0644)
if err != nil { if err != nil {
return err return err
} }
@ -232,7 +233,7 @@ var keyboardLock = sync.Mutex{}
var mouseHidFile *os.File var mouseHidFile *os.File
var mouseLock = sync.Mutex{} var mouseLock = sync.Mutex{}
func rpcKeyboardReport(modifier uint8, keys []uint8) error { func RPCKeyboardReport(modifier uint8, keys []uint8) error {
keyboardLock.Lock() keyboardLock.Lock()
defer keyboardLock.Unlock() defer keyboardLock.Unlock()
if keyboardHidFile == nil { if keyboardHidFile == nil {
@ -254,11 +255,11 @@ func rpcKeyboardReport(modifier uint8, keys []uint8) error {
keyboardHidFile = nil keyboardHidFile = nil
return err return err
} }
resetUserInputTime() ResetUserInputTime()
return err return err
} }
func rpcAbsMouseReport(x, y int, buttons uint8) error { func RPCAbsMouseReport(x, y int, buttons uint8) error {
mouseLock.Lock() mouseLock.Lock()
defer mouseLock.Unlock() defer mouseLock.Unlock()
if mouseHidFile == nil { if mouseHidFile == nil {
@ -268,7 +269,7 @@ func rpcAbsMouseReport(x, y int, buttons uint8) error {
return fmt.Errorf("failed to open hidg1: %w", err) return fmt.Errorf("failed to open hidg1: %w", err)
} }
} }
resetUserInputTime() ResetUserInputTime()
_, err := mouseHidFile.Write([]byte{ _, err := mouseHidFile.Write([]byte{
1, // Report ID 1 1, // Report ID 1
buttons, // Buttons buttons, // Buttons
@ -287,7 +288,7 @@ func rpcAbsMouseReport(x, y int, buttons uint8) error {
var accumulatedWheelY float64 = 0 var accumulatedWheelY float64 = 0
func rpcWheelReport(wheelY int8) error { func RPCWheelReport(wheelY int8) error {
if mouseHidFile == nil { if mouseHidFile == nil {
return errors.New("hid not initialized") return errors.New("hid not initialized")
} }
@ -307,7 +308,7 @@ func rpcWheelReport(wheelY int8) error {
// Reset the accumulator, keeping any remainder // Reset the accumulator, keeping any remainder
accumulatedWheelY -= float64(scaledWheelY) accumulatedWheelY -= float64(scaledWheelY)
resetUserInputTime() ResetUserInputTime()
return err return err
} }
@ -322,9 +323,9 @@ func abs(x float64) float64 {
return x return x
} }
var usbState = "unknown" var UsbState = "unknown"
func rpcGetUSBState() (state string) { func RPCGetUSBState() (state string) {
stateBytes, err := os.ReadFile("/sys/class/udc/ffb00000.usb/state") stateBytes, err := os.ReadFile("/sys/class/udc/ffb00000.usb/state")
if err != nil { if err != nil {
return "unknown" return "unknown"
@ -332,27 +333,27 @@ func rpcGetUSBState() (state string) {
return strings.TrimSpace(string(stateBytes)) return strings.TrimSpace(string(stateBytes))
} }
func triggerUSBStateUpdate() { func TriggerUSBStateUpdate() {
go func() { go func() {
if currentSession == nil { if CurrentSession == nil {
log.Println("No active RPC session, skipping update state update") log.Println("No active RPC session, skipping update state update")
return return
} }
writeJSONRPCEvent("usbState", usbState, currentSession) WriteJSONRPCEvent("usbState", UsbState, CurrentSession)
}() }()
} }
var udc string var Udc string
func init() { func init() {
go func() { go func() {
for { for {
newState := rpcGetUSBState() newState := RPCGetUSBState()
if newState != usbState { if newState != UsbState {
log.Printf("USB state changed from %s to %s", usbState, newState) log.Printf("USB state changed from %s to %s", UsbState, newState)
usbState = newState UsbState = newState
requestDisplayUpdate() RequestDisplayUpdate()
triggerUSBStateUpdate() TriggerUSBStateUpdate()
} }
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
} }

View File

@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"kvm/resource"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -16,7 +15,10 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/jetkvm/kvm/resource"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jetkvm/kvm/internal/logging"
"github.com/psanford/httpreadat" "github.com/psanford/httpreadat"
@ -40,7 +42,7 @@ func setMassStorageImage(imagePath string) error {
return nil return nil
} }
func setMassStorageMode(cdrom bool) error { func SetMassStorageMode(cdrom bool) error {
mode := "0" mode := "0"
if cdrom { if cdrom {
mode = "1" mode = "1"
@ -52,9 +54,9 @@ func setMassStorageMode(cdrom bool) error {
return nil return nil
} }
func onDiskMessage(msg webrtc.DataChannelMessage) { func OnDiskMessage(msg webrtc.DataChannelMessage) {
fmt.Println("Disk Message, len:", len(msg.Data)) fmt.Println("Disk Message, len:", len(msg.Data))
diskReadChan <- msg.Data DiskReadChan <- msg.Data
} }
func mountImage(imagePath string) error { func mountImage(imagePath string) error {
@ -73,7 +75,7 @@ var nbdDevice *NBDDevice
const imagesFolder = "/userdata/jetkvm/images" const imagesFolder = "/userdata/jetkvm/images"
func rpcMountBuiltInImage(filename string) error { func RPCMountBuiltInImage(filename string) error {
log.Println("Mount Built-In Image", filename) log.Println("Mount Built-In Image", filename)
_ = os.MkdirAll(imagesFolder, 0755) _ = os.MkdirAll(imagesFolder, 0755)
imagePath := filepath.Join(imagesFolder, filename) imagePath := filepath.Join(imagesFolder, filename)
@ -107,7 +109,7 @@ func rpcMountBuiltInImage(filename string) error {
return mountImage(imagePath) return mountImage(imagePath)
} }
func getMassStorageMode() (bool, error) { func GetMassStorageMode() (bool, error) {
data, err := os.ReadFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom")) data, err := os.ReadFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"))
if err != nil { if err != nil {
return false, fmt.Errorf("failed to read cdrom mode: %w", err) return false, fmt.Errorf("failed to read cdrom mode: %w", err)
@ -125,7 +127,7 @@ type VirtualMediaUrlInfo struct {
Size int64 Size int64
} }
func rpcCheckMountUrl(url string) (*VirtualMediaUrlInfo, error) { func RPCCheckMountUrl(url string) (*VirtualMediaUrlInfo, error) {
return nil, errors.New("not implemented") return nil, errors.New("not implemented")
} }
@ -152,18 +154,18 @@ type VirtualMediaState struct {
Size int64 `json:"size"` Size int64 `json:"size"`
} }
var currentVirtualMediaState *VirtualMediaState var CurrentVirtualMediaState *VirtualMediaState
var virtualMediaStateMutex sync.RWMutex var VirtualMediaStateMutex sync.RWMutex
func rpcGetVirtualMediaState() (*VirtualMediaState, error) { func RPCGetVirtualMediaState() (*VirtualMediaState, error) {
virtualMediaStateMutex.RLock() VirtualMediaStateMutex.RLock()
defer virtualMediaStateMutex.RUnlock() defer VirtualMediaStateMutex.RUnlock()
return currentVirtualMediaState, nil return CurrentVirtualMediaState, nil
} }
func rpcUnmountImage() error { func RPCUnmountImage() error {
virtualMediaStateMutex.Lock() VirtualMediaStateMutex.Lock()
defer virtualMediaStateMutex.Unlock() defer VirtualMediaStateMutex.Unlock()
err := setMassStorageImage("\n") err := setMassStorageImage("\n")
if err != nil { if err != nil {
fmt.Println("Remove Mass Storage Image Error", err) fmt.Println("Remove Mass Storage Image Error", err)
@ -174,92 +176,92 @@ func rpcUnmountImage() error {
nbdDevice.Close() nbdDevice.Close()
nbdDevice = nil nbdDevice = nil
} }
currentVirtualMediaState = nil CurrentVirtualMediaState = nil
return nil return nil
} }
var httpRangeReader *httpreadat.RangeReader var HttpRangeReader *httpreadat.RangeReader
func rpcMountWithHTTP(url string, mode VirtualMediaMode) error { func RPCMountWithHTTP(url string, mode VirtualMediaMode) error {
virtualMediaStateMutex.Lock() VirtualMediaStateMutex.Lock()
if currentVirtualMediaState != nil { if CurrentVirtualMediaState != nil {
virtualMediaStateMutex.Unlock() VirtualMediaStateMutex.Unlock()
return fmt.Errorf("another virtual media is already mounted") return fmt.Errorf("another virtual media is already mounted")
} }
httpRangeReader = httpreadat.New(url) HttpRangeReader = httpreadat.New(url)
n, err := httpRangeReader.Size() n, err := HttpRangeReader.Size()
if err != nil { if err != nil {
virtualMediaStateMutex.Unlock() VirtualMediaStateMutex.Unlock()
return fmt.Errorf("failed to use http url: %w", err) return fmt.Errorf("failed to use http url: %w", err)
} }
logger.Infof("using remote url %s with size %d", url, n) logging.Logger.Infof("using remote url %s with size %d", url, n)
currentVirtualMediaState = &VirtualMediaState{ CurrentVirtualMediaState = &VirtualMediaState{
Source: HTTP, Source: HTTP,
Mode: mode, Mode: mode,
URL: url, URL: url,
Size: n, Size: n,
} }
virtualMediaStateMutex.Unlock() VirtualMediaStateMutex.Unlock()
logger.Debug("Starting nbd device") logging.Logger.Debug("Starting nbd device")
nbdDevice = NewNBDDevice() nbdDevice = NewNBDDevice()
err = nbdDevice.Start() err = nbdDevice.Start()
if err != nil { if err != nil {
logger.Errorf("failed to start nbd device: %v", err) logging.Logger.Errorf("failed to start nbd device: %v", err)
return err return err
} }
logger.Debug("nbd device started") logging.Logger.Debug("nbd device started")
//TODO: replace by polling on block device having right size //TODO: replace by polling on block device having right size
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
err = setMassStorageImage("/dev/nbd0") err = setMassStorageImage("/dev/nbd0")
if err != nil { if err != nil {
return err return err
} }
logger.Info("usb mass storage mounted") logging.Logger.Info("usb mass storage mounted")
return nil return nil
} }
func rpcMountWithWebRTC(filename string, size int64, mode VirtualMediaMode) error { func RPCMountWithWebRTC(filename string, size int64, mode VirtualMediaMode) error {
virtualMediaStateMutex.Lock() VirtualMediaStateMutex.Lock()
if currentVirtualMediaState != nil { if CurrentVirtualMediaState != nil {
virtualMediaStateMutex.Unlock() VirtualMediaStateMutex.Unlock()
return fmt.Errorf("another virtual media is already mounted") return fmt.Errorf("another virtual media is already mounted")
} }
currentVirtualMediaState = &VirtualMediaState{ CurrentVirtualMediaState = &VirtualMediaState{
Source: WebRTC, Source: WebRTC,
Mode: mode, Mode: mode,
Filename: filename, Filename: filename,
Size: size, Size: size,
} }
virtualMediaStateMutex.Unlock() VirtualMediaStateMutex.Unlock()
logger.Debugf("currentVirtualMediaState is %v", currentVirtualMediaState) logging.Logger.Debugf("currentVirtualMediaState is %v", CurrentVirtualMediaState)
logger.Debug("Starting nbd device") logging.Logger.Debug("Starting nbd device")
nbdDevice = NewNBDDevice() nbdDevice = NewNBDDevice()
err := nbdDevice.Start() err := nbdDevice.Start()
if err != nil { if err != nil {
logger.Errorf("failed to start nbd device: %v", err) logging.Logger.Errorf("failed to start nbd device: %v", err)
return err return err
} }
logger.Debug("nbd device started") logging.Logger.Debug("nbd device started")
//TODO: replace by polling on block device having right size //TODO: replace by polling on block device having right size
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
err = setMassStorageImage("/dev/nbd0") err = setMassStorageImage("/dev/nbd0")
if err != nil { if err != nil {
return err return err
} }
logger.Info("usb mass storage mounted") logging.Logger.Info("usb mass storage mounted")
return nil return nil
} }
func rpcMountWithStorage(filename string, mode VirtualMediaMode) error { func RPCMountWithStorage(filename string, mode VirtualMediaMode) error {
filename, err := sanitizeFilename(filename) filename, err := sanitizeFilename(filename)
if err != nil { if err != nil {
return err return err
} }
virtualMediaStateMutex.Lock() VirtualMediaStateMutex.Lock()
defer virtualMediaStateMutex.Unlock() defer VirtualMediaStateMutex.Unlock()
if currentVirtualMediaState != nil { if CurrentVirtualMediaState != nil {
return fmt.Errorf("another virtual media is already mounted") return fmt.Errorf("another virtual media is already mounted")
} }
@ -273,7 +275,7 @@ func rpcMountWithStorage(filename string, mode VirtualMediaMode) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to set mass storage image: %w", err) return fmt.Errorf("failed to set mass storage image: %w", err)
} }
currentVirtualMediaState = &VirtualMediaState{ CurrentVirtualMediaState = &VirtualMediaState{
Source: Storage, Source: Storage,
Mode: mode, Mode: mode,
Filename: filename, Filename: filename,
@ -287,7 +289,7 @@ type StorageSpace struct {
BytesFree int64 `json:"bytesFree"` BytesFree int64 `json:"bytesFree"`
} }
func rpcGetStorageSpace() (*StorageSpace, error) { func RPCGetStorageSpace() (*StorageSpace, error) {
var stat syscall.Statfs_t var stat syscall.Statfs_t
err := syscall.Statfs(imagesFolder, &stat) err := syscall.Statfs(imagesFolder, &stat)
if err != nil { if err != nil {
@ -314,7 +316,7 @@ type StorageFiles struct {
Files []StorageFile `json:"files"` Files []StorageFile `json:"files"`
} }
func rpcListStorageFiles() (*StorageFiles, error) { func RPCListStorageFiles() (*StorageFiles, error) {
files, err := os.ReadDir(imagesFolder) files, err := os.ReadDir(imagesFolder)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read directory: %v", err) return nil, fmt.Errorf("failed to read directory: %v", err)
@ -353,7 +355,7 @@ func sanitizeFilename(filename string) (string, error) {
return sanitized, nil return sanitized, nil
} }
func rpcDeleteStorageFile(filename string) error { func RPCDeleteStorageFile(filename string) error {
sanitizedFilename, err := sanitizeFilename(filename) sanitizedFilename, err := sanitizeFilename(filename)
if err != nil { if err != nil {
return err return err
@ -378,9 +380,9 @@ type StorageFileUpload struct {
DataChannel string `json:"dataChannel"` DataChannel string `json:"dataChannel"`
} }
const uploadIdPrefix = "upload_" const UploadIdPrefix = "upload_"
func rpcStartStorageFileUpload(filename string, size int64) (*StorageFileUpload, error) { func RPCStartStorageFileUpload(filename string, size int64) (*StorageFileUpload, error) {
sanitizedFilename, err := sanitizeFilename(filename) sanitizedFilename, err := sanitizeFilename(filename)
if err != nil { if err != nil {
return nil, err return nil, err
@ -398,7 +400,7 @@ func rpcStartStorageFileUpload(filename string, size int64) (*StorageFileUpload,
alreadyUploadedBytes = stat.Size() alreadyUploadedBytes = stat.Size()
} }
uploadId := uploadIdPrefix + uuid.New().String() uploadId := UploadIdPrefix + uuid.New().String()
file, err := os.OpenFile(uploadPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) file, err := os.OpenFile(uploadPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open file for upload: %v", err) return nil, fmt.Errorf("failed to open file for upload: %v", err)
@ -430,14 +432,14 @@ type UploadProgress struct {
AlreadyUploadedBytes int64 AlreadyUploadedBytes int64
} }
func handleUploadChannel(d *webrtc.DataChannel) { func HandleUploadChannel(d *webrtc.DataChannel) {
defer d.Close() defer d.Close()
uploadId := d.Label() uploadId := d.Label()
pendingUploadsMutex.Lock() pendingUploadsMutex.Lock()
pendingUpload, ok := pendingUploads[uploadId] pendingUpload, ok := pendingUploads[uploadId]
pendingUploadsMutex.Unlock() pendingUploadsMutex.Unlock()
if !ok { if !ok {
logger.Warnf("upload channel opened for unknown upload: %s", uploadId) logging.Logger.Warnf("upload channel opened for unknown upload: %s", uploadId)
return return
} }
totalBytesWritten := pendingUpload.AlreadyUploadedBytes totalBytesWritten := pendingUpload.AlreadyUploadedBytes
@ -447,12 +449,12 @@ func handleUploadChannel(d *webrtc.DataChannel) {
newName := strings.TrimSuffix(pendingUpload.File.Name(), ".incomplete") newName := strings.TrimSuffix(pendingUpload.File.Name(), ".incomplete")
err := os.Rename(pendingUpload.File.Name(), newName) err := os.Rename(pendingUpload.File.Name(), newName)
if err != nil { if err != nil {
logger.Errorf("failed to rename uploaded file: %v", err) logging.Logger.Errorf("failed to rename uploaded file: %v", err)
} else { } else {
logger.Debugf("successfully renamed uploaded file to: %s", newName) logging.Logger.Debugf("successfully renamed uploaded file to: %s", newName)
} }
} else { } else {
logger.Warnf("uploaded ended before the complete file received") logging.Logger.Warnf("uploaded ended before the complete file received")
} }
pendingUploadsMutex.Lock() pendingUploadsMutex.Lock()
delete(pendingUploads, uploadId) delete(pendingUploads, uploadId)
@ -463,7 +465,7 @@ func handleUploadChannel(d *webrtc.DataChannel) {
d.OnMessage(func(msg webrtc.DataChannelMessage) { d.OnMessage(func(msg webrtc.DataChannelMessage) {
bytesWritten, err := pendingUpload.File.Write(msg.Data) bytesWritten, err := pendingUpload.File.Write(msg.Data)
if err != nil { if err != nil {
logger.Errorf("failed to write to file: %v", err) logging.Logger.Errorf("failed to write to file: %v", err)
close(uploadComplete) close(uploadComplete)
return return
} }
@ -485,11 +487,11 @@ func handleUploadChannel(d *webrtc.DataChannel) {
} }
progressJSON, err := json.Marshal(progress) progressJSON, err := json.Marshal(progress)
if err != nil { if err != nil {
logger.Errorf("failed to marshal upload progress: %v", err) logging.Logger.Errorf("failed to marshal upload progress: %v", err)
} else { } else {
err = d.SendText(string(progressJSON)) err = d.SendText(string(progressJSON))
if err != nil { if err != nil {
logger.Errorf("failed to send upload progress: %v", err) logging.Logger.Errorf("failed to send upload progress: %v", err)
} }
} }
lastProgressTime = time.Now() lastProgressTime = time.Now()
@ -500,7 +502,7 @@ func handleUploadChannel(d *webrtc.DataChannel) {
<-uploadComplete <-uploadComplete
} }
func handleUploadHttp(c *gin.Context) { func HandleUploadHttp(c *gin.Context) {
uploadId := c.Query("uploadId") uploadId := c.Query("uploadId")
pendingUploadsMutex.Lock() pendingUploadsMutex.Lock()
pendingUpload, ok := pendingUploads[uploadId] pendingUpload, ok := pendingUploads[uploadId]
@ -517,12 +519,12 @@ func handleUploadHttp(c *gin.Context) {
newName := strings.TrimSuffix(pendingUpload.File.Name(), ".incomplete") newName := strings.TrimSuffix(pendingUpload.File.Name(), ".incomplete")
err := os.Rename(pendingUpload.File.Name(), newName) err := os.Rename(pendingUpload.File.Name(), newName)
if err != nil { if err != nil {
logger.Errorf("failed to rename uploaded file: %v", err) logging.Logger.Errorf("failed to rename uploaded file: %v", err)
} else { } else {
logger.Debugf("successfully renamed uploaded file to: %s", newName) logging.Logger.Debugf("successfully renamed uploaded file to: %s", newName)
} }
} else { } else {
logger.Warnf("uploaded ended before the complete file received") logging.Logger.Warnf("uploaded ended before the complete file received")
} }
pendingUploadsMutex.Lock() pendingUploadsMutex.Lock()
delete(pendingUploads, uploadId) delete(pendingUploads, uploadId)
@ -534,7 +536,7 @@ func handleUploadHttp(c *gin.Context) {
for { for {
n, err := reader.Read(buffer) n, err := reader.Read(buffer)
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
logger.Errorf("failed to read from request body: %v", err) logging.Logger.Errorf("failed to read from request body: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read upload data"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read upload data"})
return return
} }
@ -542,7 +544,7 @@ func handleUploadHttp(c *gin.Context) {
if n > 0 { if n > 0 {
bytesWritten, err := pendingUpload.File.Write(buffer[:n]) bytesWritten, err := pendingUpload.File.Write(buffer[:n])
if err != nil { if err != nil {
logger.Errorf("failed to write to file: %v", err) logging.Logger.Errorf("failed to write to file: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write upload data"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write upload data"})
return return
} }

View File

@ -6,7 +6,7 @@ import (
) )
// max frame size for 1080p video, specified in mpp venc setting // max frame size for 1080p video, specified in mpp venc setting
const maxFrameSize = 1920 * 1080 / 2 const MaxFrameSize = 1920 * 1080 / 2
func writeCtrlAction(action string) error { func writeCtrlAction(action string) error {
actionMessage := map[string]string{ actionMessage := map[string]string{
@ -28,11 +28,11 @@ type VideoInputState struct {
FramePerSecond float64 `json:"fps"` FramePerSecond float64 `json:"fps"`
} }
var lastVideoState VideoInputState var LastVideoState VideoInputState
func triggerVideoStateUpdate() { func TriggerVideoStateUpdate() {
go func() { go func() {
writeJSONRPCEvent("videoInputState", lastVideoState, currentSession) WriteJSONRPCEvent("videoInputState", LastVideoState, CurrentSession)
}() }()
} }
func HandleVideoStateMessage(event CtrlResponse) { func HandleVideoStateMessage(event CtrlResponse) {
@ -42,11 +42,11 @@ func HandleVideoStateMessage(event CtrlResponse) {
log.Println("Error parsing video state json:", err) log.Println("Error parsing video state json:", err)
return return
} }
lastVideoState = videoState LastVideoState = videoState
triggerVideoStateUpdate() TriggerVideoStateUpdate()
requestDisplayUpdate() RequestDisplayUpdate()
} }
func rpcGetVideoState() (VideoInputState, error) { func rpcGetVideoState() (VideoInputState, error) {
return lastVideoState, nil return LastVideoState, nil
} }

View File

@ -10,11 +10,12 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jetkvm/kvm/internal/config"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
//go:embed all:static //go:embed static
var staticFiles embed.FS var StaticFiles embed.FS
type WebRTCSessionRequest struct { type WebRTCSessionRequest struct {
Sd string `json:"sd"` Sd string `json:"sd"`
@ -53,7 +54,7 @@ func setupRouter() *gin.Engine {
gin.DisableConsoleColor() gin.DisableConsoleColor()
r := gin.Default() r := gin.Default()
staticFS, _ := fs.Sub(staticFiles, "static") staticFS, _ := fs.Sub(StaticFiles, "static")
// Add a custom middleware to set cache headers for images // Add a custom middleware to set cache headers for images
// This is crucial for optimizing the initial welcome screen load time // This is crucial for optimizing the initial welcome screen load time
@ -83,14 +84,14 @@ func setupRouter() *gin.Engine {
protected.Use(protectedMiddleware()) protected.Use(protectedMiddleware())
{ {
protected.POST("/webrtc/session", handleWebRTCSession) protected.POST("/webrtc/session", handleWebRTCSession)
protected.POST("/cloud/register", handleCloudRegister) protected.POST("/cloud/register", HandleCloudRegister)
protected.GET("/device", handleDevice) protected.GET("/device", handleDevice)
protected.POST("/auth/logout", handleLogout) protected.POST("/auth/logout", handleLogout)
protected.POST("/auth/password-local", handleCreatePassword) protected.POST("/auth/password-local", handleCreatePassword)
protected.PUT("/auth/password-local", handleUpdatePassword) protected.PUT("/auth/password-local", handleUpdatePassword)
protected.DELETE("/auth/local-password", handleDeletePassword) protected.DELETE("/auth/local-password", handleDeletePassword)
protected.POST("/storage/upload", handleUploadHttp) protected.POST("/storage/upload", HandleUploadHttp)
} }
// Catch-all route for SPA // Catch-all route for SPA
@ -106,7 +107,7 @@ func setupRouter() *gin.Engine {
} }
// TODO: support multiple sessions? // TODO: support multiple sessions?
var currentSession *Session var CurrentSession *Session
func handleWebRTCSession(c *gin.Context) { func handleWebRTCSession(c *gin.Context) {
var req WebRTCSessionRequest var req WebRTCSessionRequest
@ -116,7 +117,7 @@ func handleWebRTCSession(c *gin.Context) {
return return
} }
session, err := newSession() session, err := NewSession()
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err}) c.JSON(http.StatusInternalServerError, gin.H{"error": err})
return return
@ -127,22 +128,22 @@ func handleWebRTCSession(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err}) c.JSON(http.StatusInternalServerError, gin.H{"error": err})
return return
} }
if currentSession != nil { if CurrentSession != nil {
writeJSONRPCEvent("otherSessionConnected", nil, currentSession) WriteJSONRPCEvent("otherSessionConnected", nil, CurrentSession)
peerConn := currentSession.peerConnection peerConn := CurrentSession.PeerConnection
go func() { go func() {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
_ = peerConn.Close() _ = peerConn.Close()
}() }()
} }
currentSession = session CurrentSession = session
c.JSON(http.StatusOK, gin.H{"sd": sd}) c.JSON(http.StatusOK, gin.H{"sd": sd})
} }
func handleLogin(c *gin.Context) { func handleLogin(c *gin.Context) {
LoadConfig() cfg := config.LoadConfig()
if config.LocalAuthMode == "noPassword" { if cfg.LocalAuthMode == "noPassword" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Login is disabled in noPassword mode"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Login is disabled in noPassword mode"})
return return
} }
@ -154,25 +155,24 @@ func handleLogin(c *gin.Context) {
return return
} }
LoadConfig() err := bcrypt.CompareHashAndPassword([]byte(cfg.HashedPassword), []byte(req.Password))
err := bcrypt.CompareHashAndPassword([]byte(config.HashedPassword), []byte(req.Password))
if err != nil { if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"})
return return
} }
config.LocalAuthToken = uuid.New().String() cfg.LocalAuthToken = uuid.New().String()
// Set the cookie // Set the cookie
c.SetCookie("authToken", config.LocalAuthToken, 7*24*60*60, "/", "", false, true) c.SetCookie("authToken", cfg.LocalAuthToken, 7*24*60*60, "/", "", false, true)
c.JSON(http.StatusOK, gin.H{"message": "Login successful"}) c.JSON(http.StatusOK, gin.H{"message": "Login successful"})
} }
func handleLogout(c *gin.Context) { func handleLogout(c *gin.Context) {
LoadConfig() cfg := config.LoadConfig()
config.LocalAuthToken = "" cfg.LocalAuthToken = ""
if err := SaveConfig(); err != nil { if err := config.SaveConfig(cfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"})
return return
} }
@ -184,15 +184,15 @@ func handleLogout(c *gin.Context) {
func protectedMiddleware() gin.HandlerFunc { func protectedMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
LoadConfig() cfg := config.LoadConfig()
if config.LocalAuthMode == "noPassword" { if cfg.LocalAuthMode == "noPassword" {
c.Next() c.Next()
return return
} }
authToken, err := c.Cookie("authToken") authToken, err := c.Cookie("authToken")
if err != nil || authToken != config.LocalAuthToken || authToken == "" { if err != nil || authToken != cfg.LocalAuthToken || authToken == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort() c.Abort()
return return
@ -214,10 +214,10 @@ func RunWebServer() {
} }
func handleDevice(c *gin.Context) { func handleDevice(c *gin.Context) {
LoadConfig() cfg := config.LoadConfig()
response := LocalDevice{ response := LocalDevice{
AuthMode: &config.LocalAuthMode, AuthMode: &cfg.LocalAuthMode,
DeviceID: GetDeviceID(), DeviceID: GetDeviceID(),
} }
@ -225,9 +225,9 @@ func handleDevice(c *gin.Context) {
} }
func handleCreatePassword(c *gin.Context) { func handleCreatePassword(c *gin.Context) {
LoadConfig() cfg := config.LoadConfig()
if config.HashedPassword != "" { if cfg.HashedPassword != "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Password already set"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Password already set"})
return return
} }
@ -235,7 +235,7 @@ func handleCreatePassword(c *gin.Context) {
// We only allow users with noPassword mode to set a new password // We only allow users with noPassword mode to set a new password
// Users with password mode are not allowed to set a new password without providing the old password // Users with password mode are not allowed to set a new password without providing the old password
// We have a PUT endpoint for changing the password, use that instead // We have a PUT endpoint for changing the password, use that instead
if config.LocalAuthMode != "noPassword" { if cfg.LocalAuthMode != "noPassword" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Password mode is not enabled"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Password mode is not enabled"})
return return
} }
@ -252,31 +252,31 @@ func handleCreatePassword(c *gin.Context) {
return return
} }
config.HashedPassword = string(hashedPassword) cfg.HashedPassword = string(hashedPassword)
config.LocalAuthToken = uuid.New().String() cfg.LocalAuthToken = uuid.New().String()
config.LocalAuthMode = "password" cfg.LocalAuthMode = "password"
if err := SaveConfig(); err != nil { if err := config.SaveConfig(cfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"})
return return
} }
// Set the cookie // Set the cookie
c.SetCookie("authToken", config.LocalAuthToken, 7*24*60*60, "/", "", false, true) c.SetCookie("authToken", cfg.LocalAuthToken, 7*24*60*60, "/", "", false, true)
c.JSON(http.StatusCreated, gin.H{"message": "Password set successfully"}) c.JSON(http.StatusCreated, gin.H{"message": "Password set successfully"})
} }
func handleUpdatePassword(c *gin.Context) { func handleUpdatePassword(c *gin.Context) {
LoadConfig() cfg := config.LoadConfig()
if config.HashedPassword == "" { if cfg.HashedPassword == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Password is not set"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Password is not set"})
return return
} }
// We only allow users with password mode to change their password // We only allow users with password mode to change their password
// Users with noPassword mode are not allowed to change their password // Users with noPassword mode are not allowed to change their password
if config.LocalAuthMode != "password" { if cfg.LocalAuthMode != "password" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Password mode is not enabled"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Password mode is not enabled"})
return return
} }
@ -287,7 +287,7 @@ func handleUpdatePassword(c *gin.Context) {
return return
} }
if err := bcrypt.CompareHashAndPassword([]byte(config.HashedPassword), []byte(req.OldPassword)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(cfg.HashedPassword), []byte(req.OldPassword)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Incorrect old password"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "Incorrect old password"})
return return
} }
@ -298,28 +298,28 @@ func handleUpdatePassword(c *gin.Context) {
return return
} }
config.HashedPassword = string(hashedPassword) cfg.HashedPassword = string(hashedPassword)
config.LocalAuthToken = uuid.New().String() cfg.LocalAuthToken = uuid.New().String()
if err := SaveConfig(); err != nil { if err := config.SaveConfig(cfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"})
return return
} }
// Set the cookie // Set the cookie
c.SetCookie("authToken", config.LocalAuthToken, 7*24*60*60, "/", "", false, true) c.SetCookie("authToken", cfg.LocalAuthToken, 7*24*60*60, "/", "", false, true)
c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"}) c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"})
} }
func handleDeletePassword(c *gin.Context) { func handleDeletePassword(c *gin.Context) {
LoadConfig() cfg := config.LoadConfig()
if config.HashedPassword == "" { if cfg.HashedPassword == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Password is not set"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Password is not set"})
return return
} }
if config.LocalAuthMode != "password" { if cfg.LocalAuthMode != "password" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Password mode is not enabled"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Password mode is not enabled"})
return return
} }
@ -330,16 +330,16 @@ func handleDeletePassword(c *gin.Context) {
return return
} }
if err := bcrypt.CompareHashAndPassword([]byte(config.HashedPassword), []byte(req.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(cfg.HashedPassword), []byte(req.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Incorrect password"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "Incorrect password"})
return return
} }
// Disable password // Disable password
config.HashedPassword = "" cfg.HashedPassword = ""
config.LocalAuthToken = "" cfg.LocalAuthToken = ""
config.LocalAuthMode = "noPassword" cfg.LocalAuthMode = "noPassword"
if err := SaveConfig(); err != nil { if err := config.SaveConfig(cfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"})
return return
} }
@ -350,20 +350,20 @@ func handleDeletePassword(c *gin.Context) {
} }
func handleDeviceStatus(c *gin.Context) { func handleDeviceStatus(c *gin.Context) {
LoadConfig() cfg := config.LoadConfig()
response := DeviceStatus{ response := DeviceStatus{
IsSetup: config.LocalAuthMode != "", IsSetup: cfg.LocalAuthMode != "",
} }
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
func handleSetup(c *gin.Context) { func handleSetup(c *gin.Context) {
LoadConfig() cfg := config.LoadConfig()
// Check if the device is already set up // Check if the device is already set up
if config.LocalAuthMode != "" || config.HashedPassword != "" { if cfg.LocalAuthMode != "" || cfg.HashedPassword != "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Device is already set up"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Device is already set up"})
return return
} }
@ -380,7 +380,7 @@ func handleSetup(c *gin.Context) {
return return
} }
config.LocalAuthMode = req.LocalAuthMode cfg.LocalAuthMode = req.LocalAuthMode
if req.LocalAuthMode == "password" { if req.LocalAuthMode == "password" {
if req.Password == "" { if req.Password == "" {
@ -395,19 +395,19 @@ func handleSetup(c *gin.Context) {
return return
} }
config.HashedPassword = string(hashedPassword) cfg.HashedPassword = string(hashedPassword)
config.LocalAuthToken = uuid.New().String() cfg.LocalAuthToken = uuid.New().String()
// Set the cookie // Set the cookie
c.SetCookie("authToken", config.LocalAuthToken, 7*24*60*60, "/", "", false, true) c.SetCookie("authToken", cfg.LocalAuthToken, 7*24*60*60, "/", "", false, true)
} else { } else {
// For noPassword mode, ensure the password field is empty // For noPassword mode, ensure the password field is empty
config.HashedPassword = "" cfg.HashedPassword = ""
config.LocalAuthToken = "" cfg.LocalAuthToken = ""
} }
err := SaveConfig() err := config.SaveConfig(cfg)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save config"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save config"})
return return

View File

@ -6,11 +6,12 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/jetkvm/kvm/internal/logging"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
) )
type Session struct { type Session struct {
peerConnection *webrtc.PeerConnection PeerConnection *webrtc.PeerConnection
VideoTrack *webrtc.TrackLocalStaticSample VideoTrack *webrtc.TrackLocalStaticSample
ControlChannel *webrtc.DataChannel ControlChannel *webrtc.DataChannel
RPCChannel *webrtc.DataChannel RPCChannel *webrtc.DataChannel
@ -30,21 +31,21 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
return "", err return "", err
} }
// Set the remote SessionDescription // Set the remote SessionDescription
if err = s.peerConnection.SetRemoteDescription(offer); err != nil { if err = s.PeerConnection.SetRemoteDescription(offer); err != nil {
return "", err return "", err
} }
// Create answer // Create answer
answer, err := s.peerConnection.CreateAnswer(nil) answer, err := s.PeerConnection.CreateAnswer(nil)
if err != nil { if err != nil {
return "", err return "", err
} }
// Create channel that is blocked until ICE Gathering is complete // Create channel that is blocked until ICE Gathering is complete
gatherComplete := webrtc.GatheringCompletePromise(s.peerConnection) gatherComplete := webrtc.GatheringCompletePromise(s.PeerConnection)
// Sets the LocalDescription, and starts our UDP listeners // Sets the LocalDescription, and starts our UDP listeners
if err = s.peerConnection.SetLocalDescription(answer); err != nil { if err = s.PeerConnection.SetLocalDescription(answer); err != nil {
return "", err return "", err
} }
@ -53,7 +54,7 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
// in a production application you should exchange ICE Candidates via OnICECandidate // in a production application you should exchange ICE Candidates via OnICECandidate
<-gatherComplete <-gatherComplete
localDescription, err := json.Marshal(s.peerConnection.LocalDescription()) localDescription, err := json.Marshal(s.PeerConnection.LocalDescription())
if err != nil { if err != nil {
return "", err return "", err
} }
@ -61,14 +62,14 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
return base64.StdEncoding.EncodeToString(localDescription), nil return base64.StdEncoding.EncodeToString(localDescription), nil
} }
func newSession() (*Session, error) { func NewSession() (*Session, error) {
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{ peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
ICEServers: []webrtc.ICEServer{{}}, ICEServers: []webrtc.ICEServer{{}},
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
session := &Session{peerConnection: peerConnection} session := &Session{PeerConnection: peerConnection}
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID()) fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID())
@ -76,19 +77,19 @@ func newSession() (*Session, error) {
case "rpc": case "rpc":
session.RPCChannel = d session.RPCChannel = d
d.OnMessage(func(msg webrtc.DataChannelMessage) { d.OnMessage(func(msg webrtc.DataChannelMessage) {
go onRPCMessage(msg, session) go OnRPCMessage(msg, session)
}) })
triggerOTAStateUpdate() TriggerOTAStateUpdate()
triggerVideoStateUpdate() TriggerVideoStateUpdate()
triggerUSBStateUpdate() TriggerUSBStateUpdate()
case "disk": case "disk":
session.DiskChannel = d session.DiskChannel = d
d.OnMessage(onDiskMessage) d.OnMessage(OnDiskMessage)
case "terminal": case "terminal":
handleTerminalChannel(d) HandleTerminalChannel(d)
default: default:
if strings.HasPrefix(d.Label(), uploadIdPrefix) { if strings.HasPrefix(d.Label(), UploadIdPrefix) {
go handleUploadChannel(d) go HandleUploadChannel(d)
} }
} }
}) })
@ -121,9 +122,9 @@ func newSession() (*Session, error) {
if connectionState == webrtc.ICEConnectionStateConnected { if connectionState == webrtc.ICEConnectionStateConnected {
if !isConnected { if !isConnected {
isConnected = true isConnected = true
actionSessions++ ActionSessions++
onActiveSessionsChanged() onActiveSessionsChanged()
if actionSessions == 1 { if ActionSessions == 1 {
onFirstSessionConnected() onFirstSessionConnected()
} }
} }
@ -133,18 +134,18 @@ func newSession() (*Session, error) {
_ = peerConnection.Close() _ = peerConnection.Close()
} }
if connectionState == webrtc.ICEConnectionStateClosed { if connectionState == webrtc.ICEConnectionStateClosed {
if session == currentSession { if session == CurrentSession {
currentSession = nil CurrentSession = nil
} }
if session.shouldUmountVirtualMedia { if session.shouldUmountVirtualMedia {
err := rpcUnmountImage() err := RPCUnmountImage()
logger.Debugf("unmount image failed on connection close %v", err) logging.Logger.Debugf("unmount image failed on connection close %v", err)
} }
if isConnected { if isConnected {
isConnected = false isConnected = false
actionSessions-- ActionSessions--
onActiveSessionsChanged() onActiveSessionsChanged()
if actionSessions == 0 { if ActionSessions == 0 {
onLastSessionDisconnected() onLastSessionDisconnected()
} }
} }
@ -153,10 +154,10 @@ func newSession() (*Session, error) {
return session, nil return session, nil
} }
var actionSessions = 0 var ActionSessions = 0
func onActiveSessionsChanged() { func onActiveSessionsChanged() {
requestDisplayUpdate() RequestDisplayUpdate()
} }
func onFirstSessionConnected() { func onFirstSessionConnected() {

View File

@ -0,0 +1,10 @@
package logging
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")
// Ideally you would implement some kind of logging system here with our own custom logging functions

View File

@ -1,4 +1,4 @@
package kvm package wol
import ( import (
"bytes" "bytes"
@ -8,7 +8,7 @@ import (
) )
// SendWOLMagicPacket sends a Wake-on-LAN magic packet to the specified MAC address // SendWOLMagicPacket sends a Wake-on-LAN magic packet to the specified MAC address
func rpcSendWOLMagicPacket(macAddress string) error { func RPCSendWolMagicPacket(macAddress string) error {
// Parse the MAC address // Parse the MAC address
mac, err := net.ParseMAC(macAddress) mac, err := net.ParseMAC(macAddress)
if err != nil { if err != nil {
@ -16,7 +16,7 @@ func rpcSendWOLMagicPacket(macAddress string) error {
} }
// Create the magic packet // Create the magic packet
packet := createMagicPacket(mac) packet := CreateMagicPacket(mac)
// Set up UDP connection // Set up UDP connection
conn, err := net.Dial("udp", "255.255.255.255:9") conn, err := net.Dial("udp", "255.255.255.255:9")
@ -34,8 +34,8 @@ func rpcSendWOLMagicPacket(macAddress string) error {
return nil return nil
} }
// createMagicPacket creates a Wake-on-LAN magic packet // CreateMagicPacket creates a Wake-on-LAN magic packet
func createMagicPacket(mac net.HardwareAddr) []byte { func CreateMagicPacket(mac net.HardwareAddr) []byte {
var buf bytes.Buffer var buf bytes.Buffer
// Write 6 bytes of 0xFF // Write 6 bytes of 0xFF

8
log.go
View File

@ -1,8 +0,0 @@
package kvm
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")

85
main.go
View File

@ -1,85 +0,0 @@
package kvm
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gwatts/rootcerts"
)
var appCtx context.Context
func Main() {
var cancel context.CancelFunc
appCtx, cancel = context.WithCancel(context.Background())
defer cancel()
logger.Info("Starting JetKvm")
go runWatchdog()
go confirmCurrentSystem()
http.DefaultClient.Timeout = 1 * time.Minute
LoadConfig()
logger.Debug("config loaded")
err := rootcerts.UpdateDefaultTransport()
if err != nil {
logger.Errorf("failed to load CA certs: %v", err)
}
go TimeSyncLoop()
StartNativeCtrlSocketServer()
StartNativeVideoSocketServer()
go func() {
err = ExtractAndRunNativeBin()
if err != nil {
logger.Errorf("failed to extract and run native bin: %v", err)
//TODO: prepare an error message screen buffer to show on kvm screen
}
}()
go func() {
time.Sleep(15 * time.Minute)
for {
logger.Debugf("UPDATING - Auto update enabled: %v", config.AutoUpdateEnabled)
if config.AutoUpdateEnabled == false {
return
}
if currentSession != nil {
logger.Debugf("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.Errorf("failed to auto update: %v", err)
}
time.Sleep(1 * time.Hour)
}
}()
//go RunFuseServer()
go RunWebServer()
go RunWebsocketClient()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
log.Println("JetKVM Shutting Down")
//if fuseServer != nil {
// err := setMassStorageImage(" ")
// if err != nil {
// log.Printf("Failed to unmount mass storage image: %v", err)
// }
// err = fuseServer.Unmount()
// if err != nil {
// log.Printf("Failed to unmount fuse: %v", err)
// }
// os.Exit(0)
}

0
publish_source.sh Executable file → Normal file
View File