mirror of https://github.com/jetkvm/kvm.git
Merge a6f20fceb1
into 8ffe66a1bc
This commit is contained in:
commit
a1f6557c43
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
* text=auto eol=lf
|
85
cmd/main.go
85
cmd/main.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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},
|
|
@ -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
|
|
@ -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()
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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
|
|
@ -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() {
|
|
@ -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
|
|
@ -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
8
log.go
|
@ -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
85
main.go
|
@ -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)
|
|
||||||
}
|
|
Loading…
Reference in New Issue