diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..0e0df62f --- /dev/null +++ b/.editorconfig @@ -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 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/cmd/main.go b/cmd/main.go index 6080affc..64a65cc7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,9 +1,90 @@ package main 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() { - 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) } diff --git a/dev_deploy.sh b/dev_deploy.sh old mode 100755 new mode 100644 diff --git a/go.mod b/go.mod index 5ddcfb68..04c8e3b8 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module kvm +module github.com/jetkvm/kvm go 1.21.0 diff --git a/config.go b/internal/config/config.go similarity index 80% rename from config.go rename to internal/config/config.go index 1636434a..8738410e 100644 --- a/config.go +++ b/internal/config/config.go @@ -1,9 +1,11 @@ -package kvm +package config import ( "encoding/json" "fmt" "os" + + "github.com/jetkvm/kvm/internal/logging" ) type WakeOnLanDevice struct { @@ -33,30 +35,31 @@ var defaultConfig = &Config{ var config *Config -func LoadConfig() { +func LoadConfig() *Config { if config != nil { - return + return config } file, err := os.Open(configPath) 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 - return + return config } defer file.Close() var loadedConfig Config 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 - return + return config } config = &loadedConfig + return config } -func SaveConfig() error { +func SaveConfig(cfg *Config) error { file, err := os.Create(configPath) if err != nil { return fmt.Errorf("failed to create config file: %w", err) @@ -65,7 +68,7 @@ func SaveConfig() error { encoder := json.NewEncoder(file) 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) } diff --git a/block_device.go b/internal/kvm/block_device.go similarity index 82% rename from block_device.go rename to internal/kvm/block_device.go index 1e348849..b19afe7e 100644 --- a/block_device.go +++ b/internal/kvm/block_device.go @@ -8,6 +8,7 @@ import ( "os" "time" + "github.com/jetkvm/kvm/internal/logging" "github.com/pojntfx/go-nbd/pkg/client" "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) { - virtualMediaStateMutex.RLock() - logger.Debugf("currentVirtualMediaState is %v", currentVirtualMediaState) - logger.Debugf("read size: %d, off: %d", len(p), off) - if currentVirtualMediaState == nil { + VirtualMediaStateMutex.RLock() + logging.Logger.Debugf("currentVirtualMediaState is %v", CurrentVirtualMediaState) + logging.Logger.Debugf("read size: %d, off: %d", len(p), off) + if CurrentVirtualMediaState == nil { return 0, errors.New("image not mounted") } - source := currentVirtualMediaState.Source - mountedImageSize := currentVirtualMediaState.Size - virtualMediaStateMutex.RUnlock() + source := CurrentVirtualMediaState.Source + mountedImageSize := CurrentVirtualMediaState.Size + VirtualMediaStateMutex.RUnlock() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -35,14 +36,14 @@ func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) { } var data []byte if source == WebRTC { - data, err = webRTCDiskReader.Read(ctx, off, readLen) + data, err = WebRTCDiskReader.Read(ctx, off, readLen) if err != nil { return 0, err } n = copy(p, data) return n, nil } else if source == HTTP { - return httpRangeReader.ReadAt(p, off) + return HttpRangeReader.ReadAt(p, off) } else { 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) { - virtualMediaStateMutex.Lock() - defer virtualMediaStateMutex.Unlock() - if currentVirtualMediaState == nil { + VirtualMediaStateMutex.Lock() + defer VirtualMediaStateMutex.Unlock() + if CurrentVirtualMediaState == nil { return 0, errors.New("no virtual media state") } - return currentVirtualMediaState.Size, nil + return CurrentVirtualMediaState.Size, nil } func (r remoteImageBackend) Sync() error { diff --git a/cloud.go b/internal/kvm/cloud.go similarity index 78% rename from cloud.go rename to internal/kvm/cloud.go index db47727d..17b0f748 100644 --- a/cloud.go +++ b/internal/kvm/cloud.go @@ -7,13 +7,16 @@ import ( "fmt" "net/http" "net/url" - "github.com/coder/websocket/wsjson" "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/gin-gonic/gin" "github.com/coder/websocket" + "github.com/gin-gonic/gin" ) type CloudRegisterRequest struct { @@ -23,7 +26,7 @@ type CloudRegisterRequest struct { ClientId string `json:"clientId"` } -func handleCloudRegister(c *gin.Context) { +func HandleCloudRegister(c *gin.Context) { var req CloudRegisterRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -68,8 +71,10 @@ func handleCloudRegister(c *gin.Context) { return } - config.CloudToken = tokenResp.SecretToken - config.CloudURL = req.CloudAPI + cfg := config.LoadConfig() + + cfg.CloudToken = tokenResp.SecretToken + cfg.CloudURL = req.CloudAPI provider, err := oidc.NewProvider(c, "https://accounts.google.com") if err != nil { @@ -88,10 +93,10 @@ func handleCloudRegister(c *gin.Context) { return } - config.GoogleIdentity = idToken.Audience[0] + ":" + idToken.Subject + cfg.GoogleIdentity = idToken.Audience[0] + ":" + idToken.Subject // 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"}) return } @@ -100,11 +105,12 @@ func handleCloudRegister(c *gin.Context) { } func runWebsocketClient() error { - if config.CloudToken == "" { + cfg := config.LoadConfig() + if cfg.CloudToken == "" { time.Sleep(5 * time.Second) return fmt.Errorf("cloud token is not set") } - wsURL, err := url.Parse(config.CloudURL) + wsURL, err := url.Parse(cfg.CloudURL) if err != nil { return fmt.Errorf("failed to parse config.CloudURL: %w", err) } @@ -115,7 +121,7 @@ func runWebsocketClient() error { } header := http.Header{} 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) defer cancelDial() c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{ @@ -125,7 +131,7 @@ func runWebsocketClient() error { return err } 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()) defer cancelRun() go func() { @@ -133,7 +139,7 @@ func runWebsocketClient() error { time.Sleep(15 * time.Second) err := c.Ping(runCtx) if err != nil { - logger.Warnf("websocket ping error: %v", err) + logging.Logger.Warnf("websocket ping error: %v", err) cancelRun() return } @@ -151,19 +157,20 @@ func runWebsocketClient() error { var req WebRTCSessionRequest err = json.Unmarshal(msg, &req) 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 } err = handleSessionRequest(runCtx, c, req) if err != nil { - logger.Infof("error starting new session: %v", err) + logging.Logger.Infof("error starting new session: %v", err) continue } } } func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error { + cfg := config.LoadConfig() oidcCtx, cancelOIDC := context.WithTimeout(ctx, time.Minute) defer cancelOIDC() 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 - if config.GoogleIdentity != googleIdentity { + if cfg.GoogleIdentity != googleIdentity { return fmt.Errorf("google identity mismatch") } - session, err := newSession() + session, err := NewSession() if err != nil { _ = wsjson.Write(context.Background(), c, gin.H{"error": 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}) return err } - if currentSession != nil { - writeJSONRPCEvent("otherSessionConnected", nil, currentSession) - peerConn := currentSession.peerConnection + if CurrentSession != nil { + WriteJSONRPCEvent("otherSessionConnected", nil, CurrentSession) + peerConn := CurrentSession.PeerConnection go func() { time.Sleep(1 * time.Second) _ = peerConn.Close() }() } - currentSession = session + CurrentSession = session _ = wsjson.Write(context.Background(), c, gin.H{"sd": sd}) return nil } @@ -226,24 +233,26 @@ type CloudState struct { URL string `json:"url,omitempty"` } -func rpcGetCloudState() CloudState { +func RPCGetCloudState() CloudState { + cfg := config.LoadConfig() return CloudState{ - Connected: config.CloudToken != "" && config.CloudURL != "", - URL: config.CloudURL, + Connected: cfg.CloudToken != "" && cfg.CloudURL != "", + URL: cfg.CloudURL, } } -func rpcDeregisterDevice() error { - if config.CloudToken == "" || config.CloudURL == "" { +func RPCDeregisterDevice() error { + cfg := config.LoadConfig() + if cfg.CloudToken == "" || cfg.CloudURL == "" { 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 { 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} resp, err := client.Do(req) 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 // (e.g., wrong cloud token, already deregistered). Regardless of the reason, we can safely remove it. if resp.StatusCode == http.StatusNotFound || (resp.StatusCode >= 200 && resp.StatusCode < 300) { - config.CloudToken = "" - config.CloudURL = "" - config.GoogleIdentity = "" - if err := SaveConfig(); err != nil { + cfg.CloudToken = "" + cfg.CloudURL = "" + cfg.GoogleIdentity = "" + if err := config.SaveConfig(cfg); err != nil { return fmt.Errorf("failed to save configuration after deregistering: %w", err) } diff --git a/display.go b/internal/kvm/display.go similarity index 89% rename from display.go rename to internal/kvm/display.go index f312eb66..0a9796ee 100644 --- a/display.go +++ b/internal/kvm/display.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "time" + ) var currentScreen = "ui_Boot_Screen" @@ -34,23 +35,23 @@ func switchToScreenIfDifferent(screenName string) { } func updateDisplay() { - updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4) - if usbState == "configured" { + updateLabelIfChanged("ui_Home_Content_Ip", NetworkState.IPv4) + if UsbState == "configured" { 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"}) } else { 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"}) } - if lastVideoState.Ready { + if LastVideoState.Ready { 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"}) } else { 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"}) } - updateLabelIfChanged("ui_Home_Header_Cloud_Status_Label", fmt.Sprintf("%d active", actionSessions)) - if networkState.Up { + updateLabelIfChanged("ui_Home_Header_Cloud_Status_Label", fmt.Sprintf("%d active", ActionSessions)) + if NetworkState.Up { switchToScreenIfDifferent("ui_Home_Screen") } else { switchToScreenIfDifferent("ui_No_Network_Screen") @@ -59,7 +60,7 @@ func updateDisplay() { var displayInited = false -func requestDisplayUpdate() { +func RequestDisplayUpdate() { if !displayInited { fmt.Println("display not inited, skipping updates") return @@ -73,7 +74,7 @@ func requestDisplayUpdate() { func updateStaticContents() { //contents that never change - updateLabelIfChanged("ui_Home_Content_Mac", networkState.MAC) + updateLabelIfChanged("ui_Home_Content_Mac", NetworkState.MAC) systemVersion, appVersion, err := GetLocalVersion() if err == nil { updateLabelIfChanged("ui_About_Content_Operating_System_Version_ContentLabel", systemVersion.String()) @@ -85,12 +86,12 @@ func updateStaticContents() { func init() { go func() { - waitCtrlClientConnected() + WaitCtrlClientConnected() fmt.Println("setting initial display contents") time.Sleep(500 * time.Millisecond) updateStaticContents() displayInited = true fmt.Println("display inited") - requestDisplayUpdate() + RequestDisplayUpdate() }() } diff --git a/fuse.go b/internal/kvm/fuse.go similarity index 96% rename from fuse.go rename to internal/kvm/fuse.go index 6ecc49c3..a045014a 100644 --- a/fuse.go +++ b/internal/kvm/fuse.go @@ -58,10 +58,10 @@ type DiskReadRequest struct { 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) { - buf, err := webRTCDiskReader.Read(ctx, off, int64(len(dest))) + buf, err := WebRTCDiskReader.Read(ctx, off, int64(len(dest))) if err != nil { return nil, syscall.EIO } diff --git a/hw.go b/internal/kvm/hardware.go similarity index 52% rename from hw.go rename to internal/kvm/hardware.go index efe8f5cb..4b7dd3ed 100644 --- a/hw.go +++ b/internal/kvm/hardware.go @@ -1,11 +1,16 @@ package kvm import ( + "context" "fmt" "os" "regexp" + "strings" "sync" "time" + + "github.com/Masterminds/semver/v3" + "github.com/jetkvm/kvm/internal/logging" ) func extractSerialNumber() (string, error) { @@ -42,7 +47,7 @@ func GetDeviceID() string { deviceIDOnce.Do(func() { serial, err := extractSerialNumber() 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" } else { deviceID = serial @@ -51,10 +56,10 @@ func GetDeviceID() string { return deviceID } -func runWatchdog() { +func RunWatchdog(ctx context.Context) { file, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0) 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 } defer file.Close() @@ -65,15 +70,36 @@ func runWatchdog() { case <-ticker.C: _, err = file.Write([]byte{0}) 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 _, err := file.Write([]byte("V")) 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 } } } + +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 +} diff --git a/jiggler.go b/internal/kvm/jiggler.go similarity index 55% rename from jiggler.go rename to internal/kvm/jiggler.go index 06f2b6c8..9f108542 100644 --- a/jiggler.go +++ b/internal/kvm/jiggler.go @@ -2,20 +2,22 @@ package kvm import ( "time" + + "github.com/jetkvm/kvm/internal/logging" ) var lastUserInput = time.Now() -func resetUserInputTime() { +func ResetUserInputTime() { lastUserInput = time.Now() } var jigglerEnabled = false -func rpcSetJigglerState(enabled bool) { +func RPCSetJigglerState(enabled bool) { jigglerEnabled = enabled } -func rpcGetJigglerState() bool { +func RPCGetJigglerState() bool { return jigglerEnabled } @@ -28,13 +30,13 @@ func runJiggler() { if jigglerEnabled { if time.Since(lastUserInput) > 20*time.Second { //TODO: change to rel mouse - err := rpcAbsMouseReport(1, 1, 0) + err := RPCAbsMouseReport(1, 1, 0) 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 { - logger.Warnf("Failed to reset mouse position: %v", err) + logging.Logger.Warnf("Failed to reset mouse position: %v", err) } } } diff --git a/jsonrpc.go b/internal/kvm/jsonrpc.go similarity index 82% rename from jsonrpc.go rename to internal/kvm/jsonrpc.go index 2ce5f189..e4bdf818 100644 --- a/jsonrpc.go +++ b/internal/kvm/jsonrpc.go @@ -11,6 +11,9 @@ import ( "path/filepath" "reflect" + "github.com/jetkvm/kvm/internal/config" + "github.com/jetkvm/kvm/internal/logging" + "github.com/jetkvm/kvm/internal/wol" "github.com/pion/webrtc/v4" ) @@ -34,7 +37,7 @@ type JSONRPCEvent struct { Params interface{} `json:"params,omitempty"` } -func writeJSONRPCResponse(response JSONRPCResponse, session *Session) { +func WriteJSONRPCResponse(response JSONRPCResponse, session *Session) { responseBytes, err := json.Marshal(response) if err != nil { 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{ JSONRPC: "2.0", 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 err := json.Unmarshal(message.Data, &request) if err != nil { @@ -81,7 +84,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { }, ID: 0, } - writeJSONRPCResponse(errorResponse, session) + WriteJSONRPCResponse(errorResponse, session) return } @@ -96,7 +99,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { }, ID: request.ID, } - writeJSONRPCResponse(errorResponse, session) + WriteJSONRPCResponse(errorResponse, session) return } @@ -111,7 +114,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { }, ID: request.ID, } - writeJSONRPCResponse(errorResponse, session) + WriteJSONRPCResponse(errorResponse, session) return } @@ -120,7 +123,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) { Result: result, ID: request.ID, } - writeJSONRPCResponse(response, session) + WriteJSONRPCResponse(response, session) } func rpcPing() (string, error) { @@ -149,13 +152,15 @@ func rpcSetStreamQualityFactor(factor float64) error { } func rpcGetAutoUpdateState() (bool, error) { - return config.AutoUpdateEnabled, nil + cfg := config.LoadConfig() + return cfg.AutoUpdateEnabled, nil } func rpcSetAutoUpdateState(enabled bool) (bool, error) { - config.AutoUpdateEnabled = enabled - if err := SaveConfig(); err != nil { - return config.AutoUpdateEnabled, fmt.Errorf("failed to save config: %w", err) + cfg := config.LoadConfig() + cfg.AutoUpdateEnabled = enabled + if err := config.SaveConfig(cfg); err != nil { + return cfg.AutoUpdateEnabled, fmt.Errorf("failed to save config: %w", err) } return enabled, nil } @@ -187,19 +192,22 @@ func rpcSetEDID(edid string) error { } func rpcGetDevChannelState() (bool, error) { - return config.IncludePreRelease, nil + cfg := config.LoadConfig() + return cfg.IncludePreRelease, nil } func rpcSetDevChannelState(enabled bool) error { - config.IncludePreRelease = enabled - if err := SaveConfig(); err != nil { + cfg := config.LoadConfig() + cfg.IncludePreRelease = enabled + if err := config.SaveConfig(cfg); err != nil { return fmt.Errorf("failed to save config: %w", err) } return nil } func rpcGetUpdateStatus() (*UpdateStatus, error) { - includePreRelease := config.IncludePreRelease + cfg := config.LoadConfig() + includePreRelease := cfg.IncludePreRelease updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease) if err != nil { return nil, fmt.Errorf("error checking for updates: %w", err) @@ -209,11 +217,12 @@ func rpcGetUpdateStatus() (*UpdateStatus, error) { } func rpcTryUpdate() error { - includePreRelease := config.IncludePreRelease + cfg := config.LoadConfig() + includePreRelease := cfg.IncludePreRelease go func() { err := TryUpdate(context.Background(), GetDeviceID(), includePreRelease) if err != nil { - logger.Warnf("failed to try update: %v", err) + logging.Logger.Warnf("failed to try update: %v", err) } }() return nil @@ -258,7 +267,7 @@ func rpcSetDevModeState(enabled bool) error { return fmt.Errorf("failed to create devmode file: %w", err) } } else { - logger.Debug("dev mode already enabled") + logging.Logger.Debug("dev mode already enabled") return nil } } else { @@ -267,7 +276,7 @@ func rpcSetDevModeState(enabled bool) error { return fmt.Errorf("failed to remove devmode file: %w", err) } } else if os.IsNotExist(err) { - logger.Debug("dev mode already disabled") + logging.Logger.Debug("dev mode already disabled") return nil } else { return fmt.Errorf("error checking dev mode file: %w", err) @@ -277,7 +286,7 @@ func rpcSetDevModeState(enabled bool) error { cmd := exec.Command("dropbear.sh") output, err := cmd.CombinedOutput() 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") } @@ -429,7 +438,7 @@ func rpcSetMassStorageMode(mode string) (string, error) { log.Printf("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode) - err := setMassStorageMode(cdrom) + err := SetMassStorageMode(cdrom) if err != nil { 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) { - cdrom, err := getMassStorageMode() + cdrom, err := GetMassStorageMode() if err != nil { return "", fmt.Errorf("failed to get mass storage mode: %w", err) } @@ -457,7 +466,7 @@ func rpcIsUpdatePending() (bool, error) { 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) { _, err := os.Stat(udcFilePath) @@ -472,34 +481,32 @@ func rpcGetUsbEmulationState() (bool, error) { func rpcSetUsbEmulationState(enabled bool) error { 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 { - 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) { - LoadConfig() - if config.WakeOnLanDevices == nil { - return []WakeOnLanDevice{}, nil +func rpcGetWakeOnLanDevices() ([]config.WakeOnLanDevice, error) { + cfg := config.LoadConfig() + if cfg.WakeOnLanDevices == nil { + return []config.WakeOnLanDevice{}, nil } - return config.WakeOnLanDevices, nil + return cfg.WakeOnLanDevices, nil } type SetWakeOnLanDevicesParams struct { - Devices []WakeOnLanDevice `json:"devices"` + Devices []config.WakeOnLanDevice `json:"devices"` } func rpcSetWakeOnLanDevices(params SetWakeOnLanDevicesParams) error { - LoadConfig() - config.WakeOnLanDevices = params.Devices - return SaveConfig() + cfg := config.LoadConfig() + cfg.WakeOnLanDevices = params.Devices + return config.SaveConfig(cfg) } func rpcResetConfig() error { - LoadConfig() - config = defaultConfig - if err := SaveConfig(); err != nil { + if err := config.SaveConfig(&config.Config{}); err != nil { return fmt.Errorf("failed to reset config: %w", err) } @@ -511,18 +518,18 @@ func rpcResetConfig() error { var rpcHandlers = map[string]RPCHandler{ "ping": {Func: rpcPing}, "getDeviceID": {Func: rpcGetDeviceID}, - "deregisterDevice": {Func: rpcDeregisterDevice}, - "getCloudState": {Func: rpcGetCloudState}, - "keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}}, - "absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}}, - "wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}}, + "deregisterDevice": {Func: RPCDeregisterDevice}, + "getCloudState": {Func: RPCGetCloudState}, + "keyboardReport": {Func: RPCKeyboardReport, Params: []string{"modifier", "keys"}}, + "absMouseReport": {Func: RPCAbsMouseReport, Params: []string{"x", "y", "buttons"}}, + "wheelReport": {Func: RPCWheelReport, Params: []string{"wheelY"}}, "getVideoState": {Func: rpcGetVideoState}, - "getUSBState": {Func: rpcGetUSBState}, - "unmountImage": {Func: rpcUnmountImage}, - "rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}}, - "setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}}, - "getJigglerState": {Func: rpcGetJigglerState}, - "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, + "getUSBState": {Func: RPCGetUSBState}, + "unmountImage": {Func: RPCUnmountImage}, + "rpcMountBuiltInImage": {Func: RPCMountBuiltInImage, Params: []string{"filename"}}, + "setJigglerState": {Func: RPCSetJigglerState, Params: []string{"enabled"}}, + "getJigglerState": {Func: RPCGetJigglerState}, + "sendWOLMagicPacket": {Func: wol.RPCSendWolMagicPacket, Params: []string{"macAddress"}}, "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, "getAutoUpdateState": {Func: rpcGetAutoUpdateState}, @@ -542,15 +549,15 @@ var rpcHandlers = map[string]RPCHandler{ "isUpdatePending": {Func: rpcIsUpdatePending}, "getUsbEmulationState": {Func: rpcGetUsbEmulationState}, "setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}}, - "checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}}, - "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, - "getStorageSpace": {Func: rpcGetStorageSpace}, - "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, - "mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, - "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, - "listStorageFiles": {Func: rpcListStorageFiles}, - "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, - "startStorageFileUpload": {Func: rpcStartStorageFileUpload, Params: []string{"filename", "size"}}, + "checkMountUrl": {Func: RPCCheckMountUrl, Params: []string{"url"}}, + "getVirtualMediaState": {Func: RPCGetVirtualMediaState}, + "getStorageSpace": {Func: RPCGetStorageSpace}, + "mountWithHTTP": {Func: RPCMountWithHTTP, Params: []string{"url", "mode"}}, + "mountWithWebRTC": {Func: RPCMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, + "mountWithStorage": {Func: RPCMountWithStorage, Params: []string{"filename", "mode"}}, + "listStorageFiles": {Func: RPCListStorageFiles}, + "deleteStorageFile": {Func: RPCDeleteStorageFile, Params: []string{"filename"}}, + "startStorageFileUpload": {Func: RPCStartStorageFileUpload, Params: []string{"filename", "size"}}, "getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices}, "setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}}, "resetConfig": {Func: rpcResetConfig}, diff --git a/native.go b/internal/kvm/native.go similarity index 81% rename from native.go rename to internal/kvm/native.go index 89e68032..d35cd080 100644 --- a/native.go +++ b/internal/kvm/native.go @@ -2,10 +2,10 @@ package kvm import ( "bytes" + "context" "encoding/json" "fmt" "io" - "kvm/resource" "log" "net" "os" @@ -13,6 +13,9 @@ import ( "sync" "time" + "github.com/jetkvm/kvm/resource" + + "github.com/jetkvm/kvm/internal/logging" "github.com/pion/webrtc/v4/pkg/media" ) @@ -95,7 +98,7 @@ var nativeVideoSocketListener net.Listener var ctrlClientConnected = make(chan struct{}) -func waitCtrlClientConnected() { +func WaitCtrlClientConnected() { <-ctrlClientConnected } @@ -118,11 +121,11 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC conn, err := listener.Accept() listener.Close() if err != nil { - logger.Errorf("failed to accept sock: %v", err) + logging.Logger.Errorf("failed to accept sock: %v", err) } if isCtrl { close(ctrlClientConnected) - logger.Debug("first native ctrl socket client connected") + logging.Logger.Debug("first native ctrl socket client connected") } handleClient(conn) }() @@ -132,20 +135,20 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC func StartNativeCtrlSocketServer() { 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() { 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) { defer conn.Close() - logger.Debug("native socket client connected") + logging.Logger.Debug("native socket client connected") if ctrlSocketConn != nil { - logger.Debugf("closing existing native socket connection") + logging.Logger.Debugf("closing existing native socket connection") ctrlSocketConn.Close() } @@ -155,15 +158,15 @@ func handleCtrlClient(conn net.Conn) { for { n, err := conn.Read(readBuf) if err != nil { - logger.Errorf("error reading from ctrl sock: %v", err) + logging.Logger.Errorf("error reading from ctrl sock: %v", err) break } readMsg := string(readBuf[:n]) - logger.Tracef("ctrl sock msg: %v", readMsg) + logging.Logger.Tracef("ctrl sock msg: %v", readMsg) ctrlResp := CtrlResponse{} err = json.Unmarshal([]byte(readMsg), &ctrlResp) if err != nil { - logger.Warnf("error parsing ctrl sock msg: %v", err) + logging.Logger.Warnf("error parsing ctrl sock msg: %v", err) continue } 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) { @@ -186,7 +189,7 @@ func handleVideoClient(conn net.Conn) { log.Printf("Native video socket client connected: %v", conn.RemoteAddr()) - inboundPacket := make([]byte, maxFrameSize) + inboundPacket := make([]byte, MaxFrameSize) lastFrame := time.Now() for { n, err := conn.Read(inboundPacket) @@ -198,8 +201,8 @@ func handleVideoClient(conn net.Conn) { sinceLastFrame := now.Sub(lastFrame) lastFrame = now //fmt.Println("Video packet received", n, sinceLastFrame) - if currentSession != nil { - err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame}) + if CurrentSession != nil { + err := CurrentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame}) if err != nil { 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" if err := ensureBinaryUpdated(binaryPath); err != nil { return fmt.Errorf("failed to extract binary: %w", err) @@ -231,11 +234,11 @@ func ExtractAndRunNativeBin() error { //TODO: add auto restart go func() { - <-appCtx.Done() - logger.Infof("killing process PID: %d", cmd.Process.Pid) + <-ctx.Done() + logging.Logger.Infof("killing process PID: %d", cmd.Process.Pid) err := cmd.Process.Kill() if err != nil { - logger.Errorf("failed to kill process: %v", err) + logging.Logger.Errorf("failed to kill process: %v", err) return } }() @@ -247,13 +250,13 @@ func ExtractAndRunNativeBin() error { func shouldOverwrite(destPath string, srcHash []byte) bool { 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 } dstHash, err := os.ReadFile(destPath + ".sha256") 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 } @@ -269,13 +272,13 @@ func ensureBinaryUpdated(destPath string) error { srcHash, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256") 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 } _, err = os.Stat(destPath) if shouldOverwrite(destPath, srcHash) || err != nil { - logger.Info("writing jetkvm_native") + logging.Logger.Info("writing jetkvm_native") _ = os.Remove(destPath) destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755) if err != nil { @@ -292,7 +295,7 @@ func ensureBinaryUpdated(destPath string) error { return err } } - logger.Info("jetkvm_native updated") + logging.Logger.Info("jetkvm_native updated") } return nil diff --git a/network.go b/internal/kvm/network.go similarity index 94% rename from network.go rename to internal/kvm/network.go index f461e453..6b44206f 100644 --- a/network.go +++ b/internal/kvm/network.go @@ -2,17 +2,17 @@ package kvm import ( "fmt" - "github.com/pion/mdns/v2" - "golang.org/x/net/ipv4" - "golang.org/x/net/ipv6" "net" "time" + "github.com/pion/mdns/v2" "github.com/vishvananda/netlink" "github.com/vishvananda/netlink/nl" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" ) -var networkState struct { +var NetworkState struct { Up bool IPv4 string IPv6 string @@ -55,10 +55,10 @@ func checkNetworkState() { } } - if newState != networkState { - networkState = newState + if newState != NetworkState { + NetworkState = newState fmt.Println("network state changed") - requestDisplayUpdate() + RequestDisplayUpdate() } } @@ -103,7 +103,7 @@ func init() { } go func() { - waitCtrlClientConnected() + WaitCtrlClientConnected() checkNetworkState() ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() diff --git a/ntp.go b/internal/kvm/ntp.go similarity index 100% rename from ntp.go rename to internal/kvm/ntp.go diff --git a/ota.go b/internal/kvm/ota.go similarity index 91% rename from ota.go rename to internal/kvm/ota.go index 9f9cb6fb..366dd50b 100644 --- a/ota.go +++ b/internal/kvm/ota.go @@ -13,10 +13,10 @@ import ( "net/url" "os" "os/exec" - "strings" "time" "github.com/Masterminds/semver/v3" + "github.com/jetkvm/kvm/internal/logging" ) type UpdateMetadata struct { @@ -43,27 +43,6 @@ type UpdateStatus struct { 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) { metadata := &UpdateMetadata{} @@ -158,7 +137,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress progress := float32(written) / float32(totalSize) if progress-*downloadProgress >= 0.01 { *downloadProgress = progress - triggerOTAStateUpdate() + TriggerOTAStateUpdate() } } if er != nil { @@ -218,7 +197,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32) error progress := float32(verified) / float32(totalSize) if progress-*verifyProgress >= 0.01 { *verifyProgress = progress - triggerOTAStateUpdate() + TriggerOTAStateUpdate() } } if er != nil { @@ -269,13 +248,13 @@ type OTAState struct { var otaState = OTAState{} -func triggerOTAStateUpdate() { +func TriggerOTAStateUpdate() { go func() { - if currentSession == nil { + if CurrentSession == nil { log.Println("No active RPC session, skipping update state update") 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{ Updating: true, } - triggerOTAStateUpdate() + TriggerOTAStateUpdate() defer func() { otaState.Updating = false - triggerOTAStateUpdate() + TriggerOTAStateUpdate() }() updateStatus, err := GetUpdateStatus(ctx, deviceId, includePreRelease) @@ -305,7 +284,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err otaState.MetadataFetchedAt = &now otaState.AppUpdatePending = updateStatus.AppUpdateAvailable otaState.SystemUpdatePending = updateStatus.SystemUpdateAvailable - triggerOTAStateUpdate() + TriggerOTAStateUpdate() local := updateStatus.Local 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) if err != nil { otaState.Error = fmt.Sprintf("Error downloading app update: %v", err) - triggerOTAStateUpdate() + TriggerOTAStateUpdate() return err } downloadFinished := time.Now() otaState.AppDownloadFinishedAt = &downloadFinished otaState.AppDownloadProgress = 1 - triggerOTAStateUpdate() + TriggerOTAStateUpdate() err = verifyFile("/userdata/jetkvm/jetkvm_app.update", remote.AppHash, &otaState.AppVerificationProgress) if err != nil { otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err) - triggerOTAStateUpdate() + TriggerOTAStateUpdate() return err } verifyFinished := time.Now() @@ -339,7 +318,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err otaState.AppVerificationProgress = 1 otaState.AppUpdatedAt = &verifyFinished otaState.AppUpdateProgress = 1 - triggerOTAStateUpdate() + TriggerOTAStateUpdate() fmt.Println("App update downloaded") 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) if err != nil { otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) - triggerOTAStateUpdate() + TriggerOTAStateUpdate() return err } downloadFinished := time.Now() otaState.SystemDownloadFinishedAt = &downloadFinished otaState.SystemDownloadProgress = 1 - triggerOTAStateUpdate() + TriggerOTAStateUpdate() err = verifyFile("/userdata/jetkvm/update_system.tar", remote.SystemHash, &otaState.SystemVerificationProgress) if err != nil { otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err) - triggerOTAStateUpdate() + TriggerOTAStateUpdate() return err } fmt.Println("System update downloaded") verifyFinished := time.Now() otaState.SystemVerifiedAt = &verifyFinished 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") var b bytes.Buffer @@ -398,7 +377,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err if otaState.SystemUpdateProgress > 0.99 { otaState.SystemUpdateProgress = 0.99 } - triggerOTAStateUpdate() + TriggerOTAStateUpdate() case <-ctx.Done(): return } @@ -416,7 +395,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err fmt.Printf("rk_ota success, output: %s\n", output) otaState.SystemUpdateProgress = 1 otaState.SystemUpdatedAt = &verifyFinished - triggerOTAStateUpdate() + TriggerOTAStateUpdate() rebootNeeded = true } else { 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 -func confirmCurrentSystem() { +func ConfirmCurrentSystem() { output, err := exec.Command("rk_ota", "--misc=now").CombinedOutput() 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)) } } diff --git a/remote_mount.go b/internal/kvm/remote_mount.go similarity index 52% rename from remote_mount.go rename to internal/kvm/remote_mount.go index e6e73226..1186a299 100644 --- a/remote_mount.go +++ b/internal/kvm/remote_mount.go @@ -4,29 +4,31 @@ import ( "context" "encoding/json" "errors" + + "github.com/jetkvm/kvm/internal/logging" ) type RemoteImageReader interface { 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) { - virtualMediaStateMutex.RLock() - if currentVirtualMediaState == nil { - virtualMediaStateMutex.RUnlock() +func (w *WebRTCDiskReaderStruct) Read(ctx context.Context, offset int64, size int64) ([]byte, error) { + VirtualMediaStateMutex.RLock() + if CurrentVirtualMediaState == nil { + VirtualMediaStateMutex.RUnlock() return nil, errors.New("image not mounted") } - if currentVirtualMediaState.Source != WebRTC { - virtualMediaStateMutex.RUnlock() + if CurrentVirtualMediaState.Source != WebRTC { + VirtualMediaStateMutex.RUnlock() return nil, errors.New("image not mounted from webrtc") } - mountedImageSize := currentVirtualMediaState.Size - virtualMediaStateMutex.RUnlock() + mountedImageSize := CurrentVirtualMediaState.Size + VirtualMediaStateMutex.RUnlock() end := offset + size if end > mountedImageSize { end = mountedImageSize @@ -40,19 +42,19 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) ( return nil, err } - if currentSession == nil || currentSession.DiskChannel == nil { + if CurrentSession == nil || CurrentSession.DiskChannel == nil { return nil, errors.New("not active session") } - logger.Debugf("reading from webrtc %v", string(jsonBytes)) - err = currentSession.DiskChannel.SendText(string(jsonBytes)) + logging.Logger.Debugf("reading from webrtc %v", string(jsonBytes)) + err = CurrentSession.DiskChannel.SendText(string(jsonBytes)) if err != nil { return nil, err } buf := make([]byte, 0) for { select { - case data := <-diskReadChan: + case data := <-DiskReadChan: buf = data[16:] case <-ctx.Done(): return nil, context.Canceled diff --git a/terminal.go b/internal/kvm/terminal.go similarity index 73% rename from terminal.go rename to internal/kvm/terminal.go index 1a1ac1cd..e6cff57a 100644 --- a/terminal.go +++ b/internal/kvm/terminal.go @@ -7,6 +7,7 @@ import ( "os/exec" "github.com/creack/pty" + "github.com/jetkvm/kvm/internal/logging" "github.com/pion/webrtc/v4" ) @@ -15,7 +16,7 @@ type TerminalSize struct { Cols int `json:"cols"` } -func handleTerminalChannel(d *webrtc.DataChannel) { +func HandleTerminalChannel(d *webrtc.DataChannel) { var ptmx *os.File var cmd *exec.Cmd d.OnOpen(func() { @@ -23,7 +24,7 @@ func handleTerminalChannel(d *webrtc.DataChannel) { var err error ptmx, err = pty.Start(cmd) if err != nil { - logger.Errorf("Failed to start pty: %v", err) + logging.Logger.Errorf("Failed to start pty: %v", err) d.Close() return } @@ -34,13 +35,13 @@ func handleTerminalChannel(d *webrtc.DataChannel) { n, err := ptmx.Read(buf) if err != nil { if err != io.EOF { - logger.Errorf("Failed to read from pty: %v", err) + logging.Logger.Errorf("Failed to read from pty: %v", err) } break } err = d.Send(buf[:n]) if err != nil { - logger.Errorf("Failed to send pty output: %v", err) + logging.Logger.Errorf("Failed to send pty output: %v", err) break } } @@ -61,11 +62,11 @@ func handleTerminalChannel(d *webrtc.DataChannel) { }) 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) if err != nil { - logger.Errorf("Failed to write to pty: %v", err) + logging.Logger.Errorf("Failed to write to pty: %v", err) } }) diff --git a/usb.go b/internal/kvm/usb.go similarity index 90% rename from usb.go rename to internal/kvm/usb.go index 075409ad..eddb3e76 100644 --- a/usb.go +++ b/internal/kvm/usb.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/jetkvm/kvm/internal/logging" gadget "github.com/openstadia/go-usb-gadget" ) @@ -37,22 +38,22 @@ func init() { _ = os.MkdirAll(imagesFolder, 0755) udcs := gadget.GetUdcs() if len(udcs) < 1 { - usbLogger.Error("no udc found, skipping USB stack init") + logging.UsbLogger.Error("no udc found, skipping USB stack init") return } - udc = udcs[0] + Udc = udcs[0] _, err := os.Stat(kvmGadgetPath) 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 } err = mountConfigFS() 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() 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 @@ -207,7 +208,7 @@ func writeGadgetConfig() error { 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 { return err } @@ -216,11 +217,11 @@ func writeGadgetConfig() 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 { 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 { return err } @@ -232,7 +233,7 @@ var keyboardLock = sync.Mutex{} var mouseHidFile *os.File var mouseLock = sync.Mutex{} -func rpcKeyboardReport(modifier uint8, keys []uint8) error { +func RPCKeyboardReport(modifier uint8, keys []uint8) error { keyboardLock.Lock() defer keyboardLock.Unlock() if keyboardHidFile == nil { @@ -254,11 +255,11 @@ func rpcKeyboardReport(modifier uint8, keys []uint8) error { keyboardHidFile = nil return err } - resetUserInputTime() + ResetUserInputTime() return err } -func rpcAbsMouseReport(x, y int, buttons uint8) error { +func RPCAbsMouseReport(x, y int, buttons uint8) error { mouseLock.Lock() defer mouseLock.Unlock() if mouseHidFile == nil { @@ -268,7 +269,7 @@ func rpcAbsMouseReport(x, y int, buttons uint8) error { return fmt.Errorf("failed to open hidg1: %w", err) } } - resetUserInputTime() + ResetUserInputTime() _, err := mouseHidFile.Write([]byte{ 1, // Report ID 1 buttons, // Buttons @@ -287,7 +288,7 @@ func rpcAbsMouseReport(x, y int, buttons uint8) error { var accumulatedWheelY float64 = 0 -func rpcWheelReport(wheelY int8) error { +func RPCWheelReport(wheelY int8) error { if mouseHidFile == nil { return errors.New("hid not initialized") } @@ -307,7 +308,7 @@ func rpcWheelReport(wheelY int8) error { // Reset the accumulator, keeping any remainder accumulatedWheelY -= float64(scaledWheelY) - resetUserInputTime() + ResetUserInputTime() return err } @@ -322,9 +323,9 @@ func abs(x float64) float64 { 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") if err != nil { return "unknown" @@ -332,27 +333,27 @@ func rpcGetUSBState() (state string) { return strings.TrimSpace(string(stateBytes)) } -func triggerUSBStateUpdate() { +func TriggerUSBStateUpdate() { go func() { - if currentSession == nil { + if CurrentSession == nil { log.Println("No active RPC session, skipping update state update") return } - writeJSONRPCEvent("usbState", usbState, currentSession) + WriteJSONRPCEvent("usbState", UsbState, CurrentSession) }() } -var udc string +var Udc string func init() { go func() { for { - newState := rpcGetUSBState() - if newState != usbState { - log.Printf("USB state changed from %s to %s", usbState, newState) - usbState = newState - requestDisplayUpdate() - triggerUSBStateUpdate() + newState := RPCGetUSBState() + if newState != UsbState { + log.Printf("USB state changed from %s to %s", UsbState, newState) + UsbState = newState + RequestDisplayUpdate() + TriggerUSBStateUpdate() } time.Sleep(500 * time.Millisecond) } diff --git a/usb_mass_storage.go b/internal/kvm/usb_mass_storage.go similarity index 76% rename from usb_mass_storage.go rename to internal/kvm/usb_mass_storage.go index b897c205..ce2f08c1 100644 --- a/usb_mass_storage.go +++ b/internal/kvm/usb_mass_storage.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "kvm/resource" "log" "net/http" "os" @@ -16,7 +15,10 @@ import ( "syscall" "time" + "github.com/jetkvm/kvm/resource" + "github.com/gin-gonic/gin" + "github.com/jetkvm/kvm/internal/logging" "github.com/psanford/httpreadat" @@ -40,7 +42,7 @@ func setMassStorageImage(imagePath string) error { return nil } -func setMassStorageMode(cdrom bool) error { +func SetMassStorageMode(cdrom bool) error { mode := "0" if cdrom { mode = "1" @@ -52,9 +54,9 @@ func setMassStorageMode(cdrom bool) error { return nil } -func onDiskMessage(msg webrtc.DataChannelMessage) { +func OnDiskMessage(msg webrtc.DataChannelMessage) { fmt.Println("Disk Message, len:", len(msg.Data)) - diskReadChan <- msg.Data + DiskReadChan <- msg.Data } func mountImage(imagePath string) error { @@ -73,7 +75,7 @@ var nbdDevice *NBDDevice const imagesFolder = "/userdata/jetkvm/images" -func rpcMountBuiltInImage(filename string) error { +func RPCMountBuiltInImage(filename string) error { log.Println("Mount Built-In Image", filename) _ = os.MkdirAll(imagesFolder, 0755) imagePath := filepath.Join(imagesFolder, filename) @@ -107,7 +109,7 @@ func rpcMountBuiltInImage(filename string) error { return mountImage(imagePath) } -func getMassStorageMode() (bool, error) { +func GetMassStorageMode() (bool, error) { data, err := os.ReadFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom")) if err != nil { return false, fmt.Errorf("failed to read cdrom mode: %w", err) @@ -125,7 +127,7 @@ type VirtualMediaUrlInfo struct { Size int64 } -func rpcCheckMountUrl(url string) (*VirtualMediaUrlInfo, error) { +func RPCCheckMountUrl(url string) (*VirtualMediaUrlInfo, error) { return nil, errors.New("not implemented") } @@ -152,18 +154,18 @@ type VirtualMediaState struct { Size int64 `json:"size"` } -var currentVirtualMediaState *VirtualMediaState -var virtualMediaStateMutex sync.RWMutex +var CurrentVirtualMediaState *VirtualMediaState +var VirtualMediaStateMutex sync.RWMutex -func rpcGetVirtualMediaState() (*VirtualMediaState, error) { - virtualMediaStateMutex.RLock() - defer virtualMediaStateMutex.RUnlock() - return currentVirtualMediaState, nil +func RPCGetVirtualMediaState() (*VirtualMediaState, error) { + VirtualMediaStateMutex.RLock() + defer VirtualMediaStateMutex.RUnlock() + return CurrentVirtualMediaState, nil } -func rpcUnmountImage() error { - virtualMediaStateMutex.Lock() - defer virtualMediaStateMutex.Unlock() +func RPCUnmountImage() error { + VirtualMediaStateMutex.Lock() + defer VirtualMediaStateMutex.Unlock() err := setMassStorageImage("\n") if err != nil { fmt.Println("Remove Mass Storage Image Error", err) @@ -174,92 +176,92 @@ func rpcUnmountImage() error { nbdDevice.Close() nbdDevice = nil } - currentVirtualMediaState = nil + CurrentVirtualMediaState = nil return nil } -var httpRangeReader *httpreadat.RangeReader +var HttpRangeReader *httpreadat.RangeReader -func rpcMountWithHTTP(url string, mode VirtualMediaMode) error { - virtualMediaStateMutex.Lock() - if currentVirtualMediaState != nil { - virtualMediaStateMutex.Unlock() +func RPCMountWithHTTP(url string, mode VirtualMediaMode) error { + VirtualMediaStateMutex.Lock() + if CurrentVirtualMediaState != nil { + VirtualMediaStateMutex.Unlock() return fmt.Errorf("another virtual media is already mounted") } - httpRangeReader = httpreadat.New(url) - n, err := httpRangeReader.Size() + HttpRangeReader = httpreadat.New(url) + n, err := HttpRangeReader.Size() if err != nil { - virtualMediaStateMutex.Unlock() + VirtualMediaStateMutex.Unlock() return fmt.Errorf("failed to use http url: %w", err) } - logger.Infof("using remote url %s with size %d", url, n) - currentVirtualMediaState = &VirtualMediaState{ + logging.Logger.Infof("using remote url %s with size %d", url, n) + CurrentVirtualMediaState = &VirtualMediaState{ Source: HTTP, Mode: mode, URL: url, Size: n, } - virtualMediaStateMutex.Unlock() + VirtualMediaStateMutex.Unlock() - logger.Debug("Starting nbd device") + logging.Logger.Debug("Starting nbd device") nbdDevice = NewNBDDevice() err = nbdDevice.Start() if err != nil { - logger.Errorf("failed to start nbd device: %v", err) + logging.Logger.Errorf("failed to start nbd device: %v", err) return err } - logger.Debug("nbd device started") + logging.Logger.Debug("nbd device started") //TODO: replace by polling on block device having right size time.Sleep(1 * time.Second) err = setMassStorageImage("/dev/nbd0") if err != nil { return err } - logger.Info("usb mass storage mounted") + logging.Logger.Info("usb mass storage mounted") return nil } -func rpcMountWithWebRTC(filename string, size int64, mode VirtualMediaMode) error { - virtualMediaStateMutex.Lock() - if currentVirtualMediaState != nil { - virtualMediaStateMutex.Unlock() +func RPCMountWithWebRTC(filename string, size int64, mode VirtualMediaMode) error { + VirtualMediaStateMutex.Lock() + if CurrentVirtualMediaState != nil { + VirtualMediaStateMutex.Unlock() return fmt.Errorf("another virtual media is already mounted") } - currentVirtualMediaState = &VirtualMediaState{ + CurrentVirtualMediaState = &VirtualMediaState{ Source: WebRTC, Mode: mode, Filename: filename, Size: size, } - virtualMediaStateMutex.Unlock() - logger.Debugf("currentVirtualMediaState is %v", currentVirtualMediaState) - logger.Debug("Starting nbd device") + VirtualMediaStateMutex.Unlock() + logging.Logger.Debugf("currentVirtualMediaState is %v", CurrentVirtualMediaState) + logging.Logger.Debug("Starting nbd device") nbdDevice = NewNBDDevice() err := nbdDevice.Start() if err != nil { - logger.Errorf("failed to start nbd device: %v", err) + logging.Logger.Errorf("failed to start nbd device: %v", err) return err } - logger.Debug("nbd device started") + logging.Logger.Debug("nbd device started") //TODO: replace by polling on block device having right size time.Sleep(1 * time.Second) err = setMassStorageImage("/dev/nbd0") if err != nil { return err } - logger.Info("usb mass storage mounted") + logging.Logger.Info("usb mass storage mounted") return nil } -func rpcMountWithStorage(filename string, mode VirtualMediaMode) error { +func RPCMountWithStorage(filename string, mode VirtualMediaMode) error { filename, err := sanitizeFilename(filename) if err != nil { return err } - virtualMediaStateMutex.Lock() - defer virtualMediaStateMutex.Unlock() - if currentVirtualMediaState != nil { + VirtualMediaStateMutex.Lock() + defer VirtualMediaStateMutex.Unlock() + if CurrentVirtualMediaState != nil { return fmt.Errorf("another virtual media is already mounted") } @@ -273,7 +275,7 @@ func rpcMountWithStorage(filename string, mode VirtualMediaMode) error { if err != nil { return fmt.Errorf("failed to set mass storage image: %w", err) } - currentVirtualMediaState = &VirtualMediaState{ + CurrentVirtualMediaState = &VirtualMediaState{ Source: Storage, Mode: mode, Filename: filename, @@ -287,7 +289,7 @@ type StorageSpace struct { BytesFree int64 `json:"bytesFree"` } -func rpcGetStorageSpace() (*StorageSpace, error) { +func RPCGetStorageSpace() (*StorageSpace, error) { var stat syscall.Statfs_t err := syscall.Statfs(imagesFolder, &stat) if err != nil { @@ -314,7 +316,7 @@ type StorageFiles struct { Files []StorageFile `json:"files"` } -func rpcListStorageFiles() (*StorageFiles, error) { +func RPCListStorageFiles() (*StorageFiles, error) { files, err := os.ReadDir(imagesFolder) if err != nil { return nil, fmt.Errorf("failed to read directory: %v", err) @@ -353,7 +355,7 @@ func sanitizeFilename(filename string) (string, error) { return sanitized, nil } -func rpcDeleteStorageFile(filename string) error { +func RPCDeleteStorageFile(filename string) error { sanitizedFilename, err := sanitizeFilename(filename) if err != nil { return err @@ -378,9 +380,9 @@ type StorageFileUpload struct { 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) if err != nil { return nil, err @@ -398,7 +400,7 @@ func rpcStartStorageFileUpload(filename string, size int64) (*StorageFileUpload, 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) if err != nil { return nil, fmt.Errorf("failed to open file for upload: %v", err) @@ -430,14 +432,14 @@ type UploadProgress struct { AlreadyUploadedBytes int64 } -func handleUploadChannel(d *webrtc.DataChannel) { +func HandleUploadChannel(d *webrtc.DataChannel) { defer d.Close() uploadId := d.Label() pendingUploadsMutex.Lock() pendingUpload, ok := pendingUploads[uploadId] pendingUploadsMutex.Unlock() if !ok { - logger.Warnf("upload channel opened for unknown upload: %s", uploadId) + logging.Logger.Warnf("upload channel opened for unknown upload: %s", uploadId) return } totalBytesWritten := pendingUpload.AlreadyUploadedBytes @@ -447,12 +449,12 @@ func handleUploadChannel(d *webrtc.DataChannel) { newName := strings.TrimSuffix(pendingUpload.File.Name(), ".incomplete") err := os.Rename(pendingUpload.File.Name(), newName) if err != nil { - logger.Errorf("failed to rename uploaded file: %v", err) + logging.Logger.Errorf("failed to rename uploaded file: %v", err) } else { - logger.Debugf("successfully renamed uploaded file to: %s", newName) + logging.Logger.Debugf("successfully renamed uploaded file to: %s", newName) } } else { - logger.Warnf("uploaded ended before the complete file received") + logging.Logger.Warnf("uploaded ended before the complete file received") } pendingUploadsMutex.Lock() delete(pendingUploads, uploadId) @@ -463,7 +465,7 @@ func handleUploadChannel(d *webrtc.DataChannel) { d.OnMessage(func(msg webrtc.DataChannelMessage) { bytesWritten, err := pendingUpload.File.Write(msg.Data) if err != nil { - logger.Errorf("failed to write to file: %v", err) + logging.Logger.Errorf("failed to write to file: %v", err) close(uploadComplete) return } @@ -485,11 +487,11 @@ func handleUploadChannel(d *webrtc.DataChannel) { } progressJSON, err := json.Marshal(progress) if err != nil { - logger.Errorf("failed to marshal upload progress: %v", err) + logging.Logger.Errorf("failed to marshal upload progress: %v", err) } else { err = d.SendText(string(progressJSON)) 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() @@ -500,7 +502,7 @@ func handleUploadChannel(d *webrtc.DataChannel) { <-uploadComplete } -func handleUploadHttp(c *gin.Context) { +func HandleUploadHttp(c *gin.Context) { uploadId := c.Query("uploadId") pendingUploadsMutex.Lock() pendingUpload, ok := pendingUploads[uploadId] @@ -517,12 +519,12 @@ func handleUploadHttp(c *gin.Context) { newName := strings.TrimSuffix(pendingUpload.File.Name(), ".incomplete") err := os.Rename(pendingUpload.File.Name(), newName) if err != nil { - logger.Errorf("failed to rename uploaded file: %v", err) + logging.Logger.Errorf("failed to rename uploaded file: %v", err) } else { - logger.Debugf("successfully renamed uploaded file to: %s", newName) + logging.Logger.Debugf("successfully renamed uploaded file to: %s", newName) } } else { - logger.Warnf("uploaded ended before the complete file received") + logging.Logger.Warnf("uploaded ended before the complete file received") } pendingUploadsMutex.Lock() delete(pendingUploads, uploadId) @@ -534,7 +536,7 @@ func handleUploadHttp(c *gin.Context) { for { n, err := reader.Read(buffer) 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"}) return } @@ -542,7 +544,7 @@ func handleUploadHttp(c *gin.Context) { if n > 0 { bytesWritten, err := pendingUpload.File.Write(buffer[:n]) 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"}) return } diff --git a/video.go b/internal/kvm/video.go similarity index 76% rename from video.go rename to internal/kvm/video.go index 8fc2bfa8..76de39c7 100644 --- a/video.go +++ b/internal/kvm/video.go @@ -6,7 +6,7 @@ import ( ) // 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 { actionMessage := map[string]string{ @@ -28,11 +28,11 @@ type VideoInputState struct { FramePerSecond float64 `json:"fps"` } -var lastVideoState VideoInputState +var LastVideoState VideoInputState -func triggerVideoStateUpdate() { +func TriggerVideoStateUpdate() { go func() { - writeJSONRPCEvent("videoInputState", lastVideoState, currentSession) + WriteJSONRPCEvent("videoInputState", LastVideoState, CurrentSession) }() } func HandleVideoStateMessage(event CtrlResponse) { @@ -42,11 +42,11 @@ func HandleVideoStateMessage(event CtrlResponse) { log.Println("Error parsing video state json:", err) return } - lastVideoState = videoState - triggerVideoStateUpdate() - requestDisplayUpdate() + LastVideoState = videoState + TriggerVideoStateUpdate() + RequestDisplayUpdate() } func rpcGetVideoState() (VideoInputState, error) { - return lastVideoState, nil + return LastVideoState, nil } diff --git a/web.go b/internal/kvm/web.go similarity index 77% rename from web.go rename to internal/kvm/web.go index 64f8de71..df6640dc 100644 --- a/web.go +++ b/internal/kvm/web.go @@ -10,11 +10,12 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/jetkvm/kvm/internal/config" "golang.org/x/crypto/bcrypt" ) -//go:embed all:static -var staticFiles embed.FS +//go:embed static +var StaticFiles embed.FS type WebRTCSessionRequest struct { Sd string `json:"sd"` @@ -53,7 +54,7 @@ func setupRouter() *gin.Engine { gin.DisableConsoleColor() r := gin.Default() - staticFS, _ := fs.Sub(staticFiles, "static") + staticFS, _ := fs.Sub(StaticFiles, "static") // Add a custom middleware to set cache headers for images // This is crucial for optimizing the initial welcome screen load time @@ -83,14 +84,14 @@ func setupRouter() *gin.Engine { protected.Use(protectedMiddleware()) { protected.POST("/webrtc/session", handleWebRTCSession) - protected.POST("/cloud/register", handleCloudRegister) + protected.POST("/cloud/register", HandleCloudRegister) protected.GET("/device", handleDevice) protected.POST("/auth/logout", handleLogout) protected.POST("/auth/password-local", handleCreatePassword) protected.PUT("/auth/password-local", handleUpdatePassword) protected.DELETE("/auth/local-password", handleDeletePassword) - protected.POST("/storage/upload", handleUploadHttp) + protected.POST("/storage/upload", HandleUploadHttp) } // Catch-all route for SPA @@ -106,7 +107,7 @@ func setupRouter() *gin.Engine { } // TODO: support multiple sessions? -var currentSession *Session +var CurrentSession *Session func handleWebRTCSession(c *gin.Context) { var req WebRTCSessionRequest @@ -116,7 +117,7 @@ func handleWebRTCSession(c *gin.Context) { return } - session, err := newSession() + session, err := NewSession() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err}) return @@ -127,22 +128,22 @@ func handleWebRTCSession(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": err}) return } - if currentSession != nil { - writeJSONRPCEvent("otherSessionConnected", nil, currentSession) - peerConn := currentSession.peerConnection + if CurrentSession != nil { + WriteJSONRPCEvent("otherSessionConnected", nil, CurrentSession) + peerConn := CurrentSession.PeerConnection go func() { time.Sleep(1 * time.Second) _ = peerConn.Close() }() } - currentSession = session + CurrentSession = session c.JSON(http.StatusOK, gin.H{"sd": sd}) } 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"}) return } @@ -154,25 +155,24 @@ func handleLogin(c *gin.Context) { return } - LoadConfig() - err := bcrypt.CompareHashAndPassword([]byte(config.HashedPassword), []byte(req.Password)) + err := bcrypt.CompareHashAndPassword([]byte(cfg.HashedPassword), []byte(req.Password)) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"}) return } - config.LocalAuthToken = uuid.New().String() + cfg.LocalAuthToken = uuid.New().String() // 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"}) } func handleLogout(c *gin.Context) { - LoadConfig() - config.LocalAuthToken = "" - if err := SaveConfig(); err != nil { + cfg := config.LoadConfig() + cfg.LocalAuthToken = "" + if err := config.SaveConfig(cfg); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"}) return } @@ -184,15 +184,15 @@ func handleLogout(c *gin.Context) { func protectedMiddleware() gin.HandlerFunc { return func(c *gin.Context) { - LoadConfig() + cfg := config.LoadConfig() - if config.LocalAuthMode == "noPassword" { + if cfg.LocalAuthMode == "noPassword" { c.Next() return } 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.Abort() return @@ -214,10 +214,10 @@ func RunWebServer() { } func handleDevice(c *gin.Context) { - LoadConfig() + cfg := config.LoadConfig() response := LocalDevice{ - AuthMode: &config.LocalAuthMode, + AuthMode: &cfg.LocalAuthMode, DeviceID: GetDeviceID(), } @@ -225,9 +225,9 @@ func handleDevice(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"}) return } @@ -235,7 +235,7 @@ func handleCreatePassword(c *gin.Context) { // 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 // 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"}) return } @@ -252,31 +252,31 @@ func handleCreatePassword(c *gin.Context) { return } - config.HashedPassword = string(hashedPassword) - config.LocalAuthToken = uuid.New().String() - config.LocalAuthMode = "password" - if err := SaveConfig(); err != nil { + cfg.HashedPassword = string(hashedPassword) + cfg.LocalAuthToken = uuid.New().String() + cfg.LocalAuthMode = "password" + if err := config.SaveConfig(cfg); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"}) return } // 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"}) } 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"}) return } // We only allow users with password mode 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"}) return } @@ -287,7 +287,7 @@ func handleUpdatePassword(c *gin.Context) { 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"}) return } @@ -298,28 +298,28 @@ func handleUpdatePassword(c *gin.Context) { return } - config.HashedPassword = string(hashedPassword) - config.LocalAuthToken = uuid.New().String() - if err := SaveConfig(); err != nil { + cfg.HashedPassword = string(hashedPassword) + cfg.LocalAuthToken = uuid.New().String() + if err := config.SaveConfig(cfg); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"}) return } // 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"}) } 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"}) return } - if config.LocalAuthMode != "password" { + if cfg.LocalAuthMode != "password" { c.JSON(http.StatusBadRequest, gin.H{"error": "Password mode is not enabled"}) return } @@ -330,16 +330,16 @@ func handleDeletePassword(c *gin.Context) { 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"}) return } // Disable password - config.HashedPassword = "" - config.LocalAuthToken = "" - config.LocalAuthMode = "noPassword" - if err := SaveConfig(); err != nil { + cfg.HashedPassword = "" + cfg.LocalAuthToken = "" + cfg.LocalAuthMode = "noPassword" + if err := config.SaveConfig(cfg); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"}) return } @@ -350,20 +350,20 @@ func handleDeletePassword(c *gin.Context) { } func handleDeviceStatus(c *gin.Context) { - LoadConfig() + cfg := config.LoadConfig() response := DeviceStatus{ - IsSetup: config.LocalAuthMode != "", + IsSetup: cfg.LocalAuthMode != "", } c.JSON(http.StatusOK, response) } func handleSetup(c *gin.Context) { - LoadConfig() + cfg := config.LoadConfig() // 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"}) return } @@ -380,7 +380,7 @@ func handleSetup(c *gin.Context) { return } - config.LocalAuthMode = req.LocalAuthMode + cfg.LocalAuthMode = req.LocalAuthMode if req.LocalAuthMode == "password" { if req.Password == "" { @@ -395,19 +395,19 @@ func handleSetup(c *gin.Context) { return } - config.HashedPassword = string(hashedPassword) - config.LocalAuthToken = uuid.New().String() + cfg.HashedPassword = string(hashedPassword) + cfg.LocalAuthToken = uuid.New().String() // 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 { // For noPassword mode, ensure the password field is empty - config.HashedPassword = "" - config.LocalAuthToken = "" + cfg.HashedPassword = "" + cfg.LocalAuthToken = "" } - err := SaveConfig() + err := config.SaveConfig(cfg) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save config"}) return diff --git a/webrtc.go b/internal/kvm/webrtc.go similarity index 75% rename from webrtc.go rename to internal/kvm/webrtc.go index 20ffb99c..e38088a6 100644 --- a/webrtc.go +++ b/internal/kvm/webrtc.go @@ -6,11 +6,12 @@ import ( "fmt" "strings" + "github.com/jetkvm/kvm/internal/logging" "github.com/pion/webrtc/v4" ) type Session struct { - peerConnection *webrtc.PeerConnection + PeerConnection *webrtc.PeerConnection VideoTrack *webrtc.TrackLocalStaticSample ControlChannel *webrtc.DataChannel RPCChannel *webrtc.DataChannel @@ -30,21 +31,21 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) { return "", err } // Set the remote SessionDescription - if err = s.peerConnection.SetRemoteDescription(offer); err != nil { + if err = s.PeerConnection.SetRemoteDescription(offer); err != nil { return "", err } // Create answer - answer, err := s.peerConnection.CreateAnswer(nil) + answer, err := s.PeerConnection.CreateAnswer(nil) if err != nil { return "", err } // 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 - if err = s.peerConnection.SetLocalDescription(answer); err != nil { + if err = s.PeerConnection.SetLocalDescription(answer); err != nil { 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 <-gatherComplete - localDescription, err := json.Marshal(s.peerConnection.LocalDescription()) + localDescription, err := json.Marshal(s.PeerConnection.LocalDescription()) if err != nil { return "", err } @@ -61,14 +62,14 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) { return base64.StdEncoding.EncodeToString(localDescription), nil } -func newSession() (*Session, error) { +func NewSession() (*Session, error) { peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{ ICEServers: []webrtc.ICEServer{{}}, }) if err != nil { return nil, err } - session := &Session{peerConnection: peerConnection} + session := &Session{PeerConnection: peerConnection} peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID()) @@ -76,19 +77,19 @@ func newSession() (*Session, error) { case "rpc": session.RPCChannel = d d.OnMessage(func(msg webrtc.DataChannelMessage) { - go onRPCMessage(msg, session) + go OnRPCMessage(msg, session) }) - triggerOTAStateUpdate() - triggerVideoStateUpdate() - triggerUSBStateUpdate() + TriggerOTAStateUpdate() + TriggerVideoStateUpdate() + TriggerUSBStateUpdate() case "disk": session.DiskChannel = d - d.OnMessage(onDiskMessage) + d.OnMessage(OnDiskMessage) case "terminal": - handleTerminalChannel(d) + HandleTerminalChannel(d) default: - if strings.HasPrefix(d.Label(), uploadIdPrefix) { - go handleUploadChannel(d) + if strings.HasPrefix(d.Label(), UploadIdPrefix) { + go HandleUploadChannel(d) } } }) @@ -121,9 +122,9 @@ func newSession() (*Session, error) { if connectionState == webrtc.ICEConnectionStateConnected { if !isConnected { isConnected = true - actionSessions++ + ActionSessions++ onActiveSessionsChanged() - if actionSessions == 1 { + if ActionSessions == 1 { onFirstSessionConnected() } } @@ -133,18 +134,18 @@ func newSession() (*Session, error) { _ = peerConnection.Close() } if connectionState == webrtc.ICEConnectionStateClosed { - if session == currentSession { - currentSession = nil + if session == CurrentSession { + CurrentSession = nil } if session.shouldUmountVirtualMedia { - err := rpcUnmountImage() - logger.Debugf("unmount image failed on connection close %v", err) + err := RPCUnmountImage() + logging.Logger.Debugf("unmount image failed on connection close %v", err) } if isConnected { isConnected = false - actionSessions-- + ActionSessions-- onActiveSessionsChanged() - if actionSessions == 0 { + if ActionSessions == 0 { onLastSessionDisconnected() } } @@ -153,10 +154,10 @@ func newSession() (*Session, error) { return session, nil } -var actionSessions = 0 +var ActionSessions = 0 func onActiveSessionsChanged() { - requestDisplayUpdate() + RequestDisplayUpdate() } func onFirstSessionConnected() { diff --git a/internal/logging/logging.go b/internal/logging/logging.go new file mode 100644 index 00000000..7eb92c9c --- /dev/null +++ b/internal/logging/logging.go @@ -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 diff --git a/wol.go b/internal/wol/wol.go similarity index 80% rename from wol.go rename to internal/wol/wol.go index 43c9e53f..8c03948d 100644 --- a/wol.go +++ b/internal/wol/wol.go @@ -1,4 +1,4 @@ -package kvm +package wol import ( "bytes" @@ -8,7 +8,7 @@ import ( ) // 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 mac, err := net.ParseMAC(macAddress) if err != nil { @@ -16,7 +16,7 @@ func rpcSendWOLMagicPacket(macAddress string) error { } // Create the magic packet - packet := createMagicPacket(mac) + packet := CreateMagicPacket(mac) // Set up UDP connection conn, err := net.Dial("udp", "255.255.255.255:9") @@ -34,8 +34,8 @@ func rpcSendWOLMagicPacket(macAddress string) error { return nil } -// createMagicPacket creates a Wake-on-LAN magic packet -func createMagicPacket(mac net.HardwareAddr) []byte { +// CreateMagicPacket creates a Wake-on-LAN magic packet +func CreateMagicPacket(mac net.HardwareAddr) []byte { var buf bytes.Buffer // Write 6 bytes of 0xFF diff --git a/log.go b/log.go deleted file mode 100644 index 89ad1d2e..00000000 --- a/log.go +++ /dev/null @@ -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") diff --git a/main.go b/main.go deleted file mode 100644 index f94b24eb..00000000 --- a/main.go +++ /dev/null @@ -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) -} diff --git a/bin/.gitkeep b/pkg/.gitkeep similarity index 100% rename from bin/.gitkeep rename to pkg/.gitkeep diff --git a/publish_source.sh b/publish_source.sh old mode 100755 new mode 100644