mirror of https://github.com/jetkvm/kvm.git
feat: move native to a separate process, again (#964)
This commit is contained in:
parent
87eb555fa2
commit
5717761d16
|
|
@ -17,6 +17,7 @@ sudo apt-get install -y --no-install-recommends \
|
|||
build-essential \
|
||||
device-tree-compiler \
|
||||
gperf g++-multilib gcc-multilib \
|
||||
gdb-multiarch \
|
||||
libnl-3-dev libdbus-1-dev libelf-dev libmpc-dev dwarves \
|
||||
bc openssl flex bison libssl-dev python3 python-is-python3 texinfo kmod cmake \
|
||||
wget zstd \
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Linux",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**"
|
||||
],
|
||||
"defines": [],
|
||||
"compilerPath": "/opt/jetkvm-native-buildkit/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc",
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "gnu++17",
|
||||
"intelliSenseMode": "linux-gcc-arm",
|
||||
"configurationProvider": "ms-vscode.cmake-tools"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "GDB Debug - Native (binary)",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "internal/native/cgo/build/jknative-bin",
|
||||
"args": [],
|
||||
"stopAtEntry": true,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"environment": [],
|
||||
"MIMode": "gdb",
|
||||
"miDebuggerPath": "/usr/bin/gdb-multiarch",
|
||||
"miDebuggerServerAddress": "${config:TARGET_IP}:${config:DEBUG_PORT}",
|
||||
"targetArchitecture": "arm",
|
||||
"preLaunchTask": "deploy",
|
||||
"setupCommands": [
|
||||
{
|
||||
"description": "Pretty-printing for gdb",
|
||||
"text": "-enable-pretty-printing",
|
||||
"ignoreFailures": true
|
||||
}
|
||||
],
|
||||
"externalConsole": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -10,8 +10,13 @@
|
|||
]
|
||||
},
|
||||
"git.ignoreLimitWarning": true,
|
||||
"cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo",
|
||||
"cmake.sourceDirectory": "${workspaceFolder}/internal/native/cgo",
|
||||
"cmake.ignoreCMakeListsMissing": true,
|
||||
"C_Cpp.inlayHints.autoDeclarationTypes.enabled": true,
|
||||
"C_Cpp.inlayHints.parameterNames.enabled": true,
|
||||
"C_Cpp.inlayHints.referenceOperator.enabled": true,
|
||||
"TARGET_IP": "192.168.0.199",
|
||||
"DEBUG_PORT": "2345",
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "deploy",
|
||||
"isBackground": true,
|
||||
"type": "shell",
|
||||
"command": "bash",
|
||||
"args": [
|
||||
"dev_deploy.sh",
|
||||
"-r",
|
||||
"${config:TARGET_IP}",
|
||||
"--gdb-port",
|
||||
"${config:DEBUG_PORT}",
|
||||
"--native-binary",
|
||||
"--disable-docker"
|
||||
],
|
||||
"problemMatcher": {
|
||||
"base": "$gcc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "${config:BINARY}",
|
||||
"endsPattern": "Listening on port [0-9]{4}"
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -203,6 +203,12 @@ rm /userdata/kvm_config.json
|
|||
systemctl restart jetkvm
|
||||
```
|
||||
|
||||
### Debug native code with gdbserver
|
||||
|
||||
Change the `TARGET_IP` in `.vscode/settings.json` to your JetKVM device IP, then set breakpoints in your native code and start the `Debug Native` configuration in VSCode.
|
||||
|
||||
The code and GDB server will be deployed automatically.
|
||||
|
||||
---
|
||||
|
||||
## Testing Your Changes
|
||||
|
|
|
|||
5
Makefile
5
Makefile
|
|
@ -12,6 +12,10 @@ BUILDKIT_FLAVOR := arm-rockchip830-linux-uclibcgnueabihf
|
|||
BUILDKIT_PATH ?= /opt/jetkvm-native-buildkit
|
||||
SKIP_NATIVE_IF_EXISTS ?= 0
|
||||
SKIP_UI_BUILD ?= 0
|
||||
ENABLE_SYNC_TRACE ?= 0
|
||||
|
||||
CMAKE_BUILD_TYPE ?= Release
|
||||
|
||||
GO_BUILD_ARGS := -tags netgo,timetzdata,nomsgpack
|
||||
GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS)
|
||||
GO_LDFLAGS := \
|
||||
|
|
@ -46,6 +50,7 @@ build_native:
|
|||
echo "Building native..."; \
|
||||
CC="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-gcc" \
|
||||
LD="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-ld" \
|
||||
CMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) \
|
||||
./scripts/build_cgo.sh; \
|
||||
fi
|
||||
|
||||
|
|
|
|||
54
cmd/main.go
54
cmd/main.go
|
|
@ -13,23 +13,39 @@ import (
|
|||
|
||||
"github.com/erikdubbelboer/gspt"
|
||||
"github.com/jetkvm/kvm"
|
||||
"github.com/jetkvm/kvm/internal/native"
|
||||
"github.com/jetkvm/kvm/internal/supervisor"
|
||||
)
|
||||
|
||||
const (
|
||||
envChildID = "JETKVM_CHILD_ID"
|
||||
errorDumpDir = "/userdata/jetkvm/crashdump"
|
||||
errorDumpLastFile = "last-crash.log"
|
||||
errorDumpTemplate = "jetkvm-%s.log"
|
||||
var (
|
||||
subcomponent string
|
||||
)
|
||||
|
||||
func program() {
|
||||
gspt.SetProcTitle(os.Args[0] + " [app]")
|
||||
kvm.Main()
|
||||
subcomponentOverride := os.Getenv(supervisor.EnvSubcomponent)
|
||||
if subcomponentOverride != "" {
|
||||
subcomponent = subcomponentOverride
|
||||
}
|
||||
switch subcomponent {
|
||||
case "native":
|
||||
native.RunNativeProcess(os.Args[0])
|
||||
default:
|
||||
kvm.Main()
|
||||
}
|
||||
}
|
||||
|
||||
func setProcTitle(status string) {
|
||||
if status != "" {
|
||||
status = " " + status
|
||||
}
|
||||
title := fmt.Sprintf("jetkvm: [supervisor]%s", status)
|
||||
gspt.SetProcTitle(title)
|
||||
}
|
||||
|
||||
func main() {
|
||||
versionPtr := flag.Bool("version", false, "print version and exit")
|
||||
versionJSONPtr := flag.Bool("version-json", false, "print version as json and exit")
|
||||
flag.StringVar(&subcomponent, "subcomponent", "", "subcomponent to run")
|
||||
flag.Parse()
|
||||
|
||||
if *versionPtr || *versionJSONPtr {
|
||||
|
|
@ -42,7 +58,7 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
childID := os.Getenv(envChildID)
|
||||
childID := os.Getenv(supervisor.EnvChildID)
|
||||
switch childID {
|
||||
case "":
|
||||
doSupervise()
|
||||
|
|
@ -55,6 +71,8 @@ func main() {
|
|||
}
|
||||
|
||||
func supervise() error {
|
||||
setProcTitle("")
|
||||
|
||||
// check binary path
|
||||
binPath, err := os.Executable()
|
||||
if err != nil {
|
||||
|
|
@ -74,11 +92,11 @@ func supervise() error {
|
|||
// run the child binary
|
||||
cmd := exec.Command(binPath)
|
||||
|
||||
lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile)
|
||||
lastFilePath := filepath.Join(supervisor.ErrorDumpDir, supervisor.ErrorDumpLastFile)
|
||||
|
||||
cmd.Env = append(os.Environ(), []string{
|
||||
fmt.Sprintf("%s=%s", envChildID, kvm.GetBuiltAppVersion()),
|
||||
fmt.Sprintf("JETKVM_LAST_ERROR_PATH=%s", lastFilePath),
|
||||
fmt.Sprintf("%s=%s", supervisor.EnvChildID, kvm.GetBuiltAppVersion()),
|
||||
fmt.Sprintf("%s=%s", supervisor.ErrorDumpLastFile, lastFilePath),
|
||||
}...)
|
||||
cmd.Args = os.Args
|
||||
|
||||
|
|
@ -99,6 +117,8 @@ func supervise() error {
|
|||
return fmt.Errorf("failed to start command: %w", startErr)
|
||||
}
|
||||
|
||||
setProcTitle(fmt.Sprintf("started (pid=%d)", cmd.Process.Pid))
|
||||
|
||||
go func() {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGTERM)
|
||||
|
|
@ -107,8 +127,6 @@ func supervise() error {
|
|||
_ = cmd.Process.Signal(sig)
|
||||
}()
|
||||
|
||||
gspt.SetProcTitle(os.Args[0] + " [sup]")
|
||||
|
||||
cmdErr := cmd.Wait()
|
||||
if cmdErr == nil {
|
||||
return nil
|
||||
|
|
@ -186,11 +204,11 @@ func renameFile(f *os.File, newName string) error {
|
|||
|
||||
func ensureErrorDumpDir() error {
|
||||
// TODO: check if the directory is writable
|
||||
f, err := os.Stat(errorDumpDir)
|
||||
f, err := os.Stat(supervisor.ErrorDumpDir)
|
||||
if err == nil && f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(errorDumpDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(supervisor.ErrorDumpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create error dump directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -200,7 +218,7 @@ func createErrorDump(logFile *os.File) {
|
|||
fmt.Println()
|
||||
|
||||
fileName := fmt.Sprintf(
|
||||
errorDumpTemplate,
|
||||
supervisor.ErrorDumpTemplate,
|
||||
time.Now().Format("20060102-150405"),
|
||||
)
|
||||
|
||||
|
|
@ -210,7 +228,7 @@ func createErrorDump(logFile *os.File) {
|
|||
return
|
||||
}
|
||||
|
||||
filePath := filepath.Join(errorDumpDir, fileName)
|
||||
filePath := filepath.Join(supervisor.ErrorDumpDir, fileName)
|
||||
if err := renameFile(logFile, filePath); err != nil {
|
||||
fmt.Printf("failed to rename file: %v\n", err)
|
||||
return
|
||||
|
|
@ -218,7 +236,7 @@ func createErrorDump(logFile *os.File) {
|
|||
|
||||
fmt.Printf("error dump copied: %s\n", filePath)
|
||||
|
||||
lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile)
|
||||
lastFilePath := filepath.Join(supervisor.ErrorDumpDir, supervisor.ErrorDumpLastFile)
|
||||
|
||||
if err := ensureSymlink(filePath, lastFilePath); err != nil {
|
||||
fmt.Printf("failed to create symlink: %v\n", err)
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ type Config struct {
|
|||
DefaultLogLevel string `json:"default_log_level"`
|
||||
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
|
||||
VideoQualityFactor float64 `json:"video_quality_factor"`
|
||||
NativeMaxRestart uint `json:"native_max_restart_attempts"`
|
||||
}
|
||||
|
||||
// GetUpdateAPIURL returns the update API URL
|
||||
|
|
|
|||
|
|
@ -218,6 +218,14 @@ func updateStaticContents() {
|
|||
// nativeInstance.UpdateLabelAndChangeVisibility("boot_screen_device_id", GetDeviceID())
|
||||
}
|
||||
|
||||
// configureDisplayOnNativeRestart is called when the native process restarts
|
||||
// it ensures the display is configured correctly after the restart
|
||||
func configureDisplayOnNativeRestart() {
|
||||
displayLogger.Info().Msg("native restarted, configuring display")
|
||||
updateStaticContents()
|
||||
requestDisplayUpdate(true, "native_restart")
|
||||
}
|
||||
|
||||
// setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter
|
||||
// the backlight brightness of the JetKVM hardware's display.
|
||||
func setDisplayBrightness(brightness int, reason string) error {
|
||||
|
|
|
|||
|
|
@ -77,10 +77,12 @@ func checkFailsafeReason() {
|
|||
_ = os.Remove(lastCrashPath)
|
||||
|
||||
// TODO: read the goroutine stack trace and check which goroutine is panicking
|
||||
failsafeModeActive = true
|
||||
if strings.Contains(failsafeCrashLog, "runtime.cgocall") {
|
||||
failsafeModeActive = true
|
||||
failsafeModeReason = "video"
|
||||
return
|
||||
} else {
|
||||
failsafeModeReason = "unknown"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
12
go.mod
12
go.mod
|
|
@ -42,6 +42,7 @@ require (
|
|||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/caarlos0/env/v11 v11.3.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/creack/goselect v0.1.2 // indirect
|
||||
|
|
@ -81,14 +82,19 @@ require (
|
|||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
26
go.sum
26
go.sum
|
|
@ -12,6 +12,8 @@ github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQ
|
|||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs=
|
||||
|
|
@ -158,6 +160,8 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
|||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
|
@ -198,14 +202,20 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# jetkvm-native
|
||||
|
||||
This component (`internal/native/`) acts as a bridge between Golang and native (C/C++) code.
|
||||
It manages spawning and communicating with a native process via sockets (gRPC and Unix stream).
|
||||
|
||||
For performance-critical operations such as video frame, **a dedicated Unix socket should be used** to avoid the overhead of gRPC and ensure low-latency communication.
|
||||
|
||||
## Debugging
|
||||
|
||||
To enable debug mode, create a file called `.native-debug-mode` in the `/userdata/jetkvm` directory.
|
||||
|
||||
```bash
|
||||
touch /userdata/jetkvm/.native-debug-mode
|
||||
```
|
||||
|
||||
This will cause the native process to listen for SIGHUP signal and crash the process.
|
||||
|
||||
```bash
|
||||
pgrep native | xargs kill -SIGHUP
|
||||
```
|
||||
|
|
@ -42,6 +42,8 @@ FetchContent_MakeAvailable(lvgl)
|
|||
# Get source files, excluding CMake generated files
|
||||
file(GLOB_RECURSE sources CONFIGURE_DEPENDS "*.c" "ui/*.c")
|
||||
list(FILTER sources EXCLUDE REGEX "CMakeFiles.*CompilerId.*\\.c$")
|
||||
# Exclude main.c from library sources (it's used for the binary target)
|
||||
list(FILTER sources EXCLUDE REGEX "main\\.c$")
|
||||
|
||||
add_library(jknative STATIC ${sources} ${CMAKE_CURRENT_SOURCE_DIR}/ctrl.h)
|
||||
|
||||
|
|
@ -68,4 +70,14 @@ target_link_libraries(jknative PRIVATE
|
|||
# libgpiod
|
||||
)
|
||||
|
||||
install(TARGETS jknative DESTINATION lib)
|
||||
# Binary target using main.c as entry point
|
||||
add_executable(jknative-bin ${CMAKE_CURRENT_SOURCE_DIR}/main.c)
|
||||
|
||||
# Link the binary to the library (if needed in the future)
|
||||
target_link_libraries(jknative-bin PRIVATE
|
||||
jknative
|
||||
pthread
|
||||
)
|
||||
|
||||
install(TARGETS jknative DESTINATION lib)
|
||||
install(TARGETS jknative-bin DESTINATION bin)
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/select.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include "ctrl.h"
|
||||
#include "main.h"
|
||||
|
||||
#define SOCKET_PATH "/tmp/video.sock"
|
||||
#define BUFFER_SIZE 4096
|
||||
|
||||
// Global state
|
||||
static int client_fd = -1;
|
||||
static pthread_mutex_t client_fd_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
void jetkvm_c_log_handler(int level, const char *filename, const char *funcname, int line, const char *message) {
|
||||
// printf("[%s] %s:%d %s: %s\n", filename ? filename : "unknown", funcname ? funcname : "unknown", line, message ? message : "");
|
||||
fprintf(stderr, "[%s] %s:%d %s: %s\n", filename ? filename : "unknown", funcname ? funcname : "unknown", line, message ? message : "");
|
||||
}
|
||||
|
||||
// Video handler that pipes frames to the Unix socket
|
||||
// This will be called by the video subsystem via video_send_frame -> jetkvm_set_video_handler's handler
|
||||
void jetkvm_video_handler(const uint8_t *frame, ssize_t len) {
|
||||
// pthread_mutex_lock(&client_fd_mutex);
|
||||
// if (client_fd >= 0 && frame != NULL && len > 0) {
|
||||
// ssize_t bytes_written = 0;
|
||||
// while (bytes_written < len) {
|
||||
// ssize_t n = write(client_fd, frame + bytes_written, len - bytes_written);
|
||||
// if (n < 0) {
|
||||
// if (errno == EPIPE || errno == ECONNRESET) {
|
||||
// // Client disconnected
|
||||
// close(client_fd);
|
||||
// client_fd = -1;
|
||||
// break;
|
||||
// }
|
||||
// perror("write");
|
||||
// break;
|
||||
// }
|
||||
// bytes_written += n;
|
||||
// }
|
||||
// }
|
||||
// pthread_mutex_unlock(&client_fd_mutex);
|
||||
}
|
||||
|
||||
void jetkvm_video_state_handler(jetkvm_video_state_t *state) {
|
||||
fprintf(stderr, "Video state: {\n"
|
||||
"\"ready\": %d,\n"
|
||||
"\"error\": \"%s\",\n"
|
||||
"\"width\": %d,\n"
|
||||
"\"height\": %d,\n"
|
||||
"\"frame_per_second\": %f\n"
|
||||
"}\n", state->ready, state->error, state->width, state->height, state->frame_per_second);
|
||||
}
|
||||
|
||||
void jetkvm_indev_handler(int code) {
|
||||
fprintf(stderr, "Video indev: %d\n", code);
|
||||
}
|
||||
|
||||
void jetkvm_rpc_handler(const char *method, const char *params) {
|
||||
fprintf(stderr, "Video rpc: %s %s\n", method, params);
|
||||
}
|
||||
|
||||
// Note: jetkvm_set_video_handler, jetkvm_set_indev_handler, jetkvm_set_rpc_handler,
|
||||
// jetkvm_call_rpc_handler, and jetkvm_set_video_state_handler are implemented in
|
||||
// the library (ctrl.c) and will be used from there when linking.
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
const char *socket_path = SOCKET_PATH;
|
||||
|
||||
// Allow custom socket path via command line argument
|
||||
if (argc > 1) {
|
||||
socket_path = argv[1];
|
||||
}
|
||||
|
||||
// Remove existing socket file if it exists
|
||||
unlink(socket_path);
|
||||
|
||||
// Set handlers
|
||||
jetkvm_set_log_handler(&jetkvm_c_log_handler);
|
||||
jetkvm_set_video_handler(&jetkvm_video_handler);
|
||||
jetkvm_set_video_state_handler(&jetkvm_video_state_handler);
|
||||
jetkvm_set_indev_handler(&jetkvm_indev_handler);
|
||||
jetkvm_set_rpc_handler(&jetkvm_rpc_handler);
|
||||
|
||||
// Initialize video first (before accepting connections)
|
||||
fprintf(stderr, "Initializing video...\n");
|
||||
if (jetkvm_video_init(1.0) != 0) {
|
||||
fprintf(stderr, "Failed to initialize video\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Start video streaming - frames will be sent via video_send_frame
|
||||
// which calls the video handler we set up
|
||||
jetkvm_video_start();
|
||||
fprintf(stderr, "Video streaming started.\n");
|
||||
|
||||
// Create Unix domain socket
|
||||
int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (server_fd < 0) {
|
||||
perror("socket");
|
||||
jetkvm_video_stop();
|
||||
jetkvm_video_shutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Make socket non-blocking
|
||||
int flags = fcntl(server_fd, F_GETFL, 0);
|
||||
if (flags < 0 || fcntl(server_fd, F_SETFL, flags | O_NONBLOCK) < 0) {
|
||||
perror("fcntl");
|
||||
close(server_fd);
|
||||
jetkvm_video_stop();
|
||||
jetkvm_video_shutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Bind socket to path
|
||||
struct sockaddr_un addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sun_family = AF_UNIX;
|
||||
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
|
||||
|
||||
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
perror("bind");
|
||||
close(server_fd);
|
||||
jetkvm_video_stop();
|
||||
jetkvm_video_shutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Listen for connections
|
||||
if (listen(server_fd, 1) < 0) {
|
||||
perror("listen");
|
||||
close(server_fd);
|
||||
jetkvm_video_stop();
|
||||
jetkvm_video_shutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Listening on Unix socket: %s (non-blocking)\n", socket_path);
|
||||
fprintf(stderr, "Video frames will be sent to connected clients...\n");
|
||||
|
||||
// Main loop: check for new connections and handle client disconnections
|
||||
fd_set read_fds;
|
||||
struct timeval timeout;
|
||||
|
||||
while (1) {
|
||||
FD_ZERO(&read_fds);
|
||||
FD_SET(server_fd, &read_fds);
|
||||
|
||||
pthread_mutex_lock(&client_fd_mutex);
|
||||
int current_client_fd = client_fd;
|
||||
if (current_client_fd >= 0) {
|
||||
FD_SET(current_client_fd, &read_fds);
|
||||
}
|
||||
int max_fd = (current_client_fd > server_fd) ? current_client_fd : server_fd;
|
||||
pthread_mutex_unlock(&client_fd_mutex);
|
||||
|
||||
timeout.tv_sec = 1;
|
||||
timeout.tv_usec = 0;
|
||||
|
||||
int result = select(max_fd + 1, &read_fds, NULL, NULL, &timeout);
|
||||
if (result < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
perror("select");
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for new connection
|
||||
if (FD_ISSET(server_fd, &read_fds)) {
|
||||
int accepted_fd = accept(server_fd, NULL, NULL);
|
||||
if (accepted_fd >= 0) {
|
||||
fprintf(stderr, "Client connected\n");
|
||||
pthread_mutex_lock(&client_fd_mutex);
|
||||
if (client_fd >= 0) {
|
||||
// Close previous client if any
|
||||
close(client_fd);
|
||||
}
|
||||
client_fd = accepted_fd;
|
||||
pthread_mutex_unlock(&client_fd_mutex);
|
||||
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
perror("accept");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if client disconnected
|
||||
pthread_mutex_lock(&client_fd_mutex);
|
||||
current_client_fd = client_fd;
|
||||
pthread_mutex_unlock(&client_fd_mutex);
|
||||
|
||||
if (current_client_fd >= 0 && FD_ISSET(current_client_fd, &read_fds)) {
|
||||
// Client sent data or closed connection
|
||||
char buffer[1];
|
||||
if (read(current_client_fd, buffer, 1) <= 0) {
|
||||
fprintf(stderr, "Client disconnected\n");
|
||||
pthread_mutex_lock(&client_fd_mutex);
|
||||
close(client_fd);
|
||||
client_fd = -1;
|
||||
pthread_mutex_unlock(&client_fd_mutex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop video streaming
|
||||
jetkvm_video_stop();
|
||||
jetkvm_video_shutdown();
|
||||
|
||||
// Cleanup
|
||||
pthread_mutex_lock(&client_fd_mutex);
|
||||
if (client_fd >= 0) {
|
||||
close(client_fd);
|
||||
client_fd = -1;
|
||||
}
|
||||
pthread_mutex_unlock(&client_fd_mutex);
|
||||
|
||||
close(server_fd);
|
||||
unlink(socket_path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
#ifndef JETKVM_NATIVE_MAIN_H
|
||||
#define JETKVM_NATIVE_MAIN_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <errno.h>
|
||||
#include "ctrl.h"
|
||||
|
||||
void jetkvm_c_log_handler(int level, const char *filename, const char *funcname, int line, const char *message);
|
||||
void jetkvm_video_handler(const uint8_t *frame, ssize_t len);
|
||||
void jetkvm_video_state_handler(jetkvm_video_state_t *state);
|
||||
void jetkvm_indev_handler(int code);
|
||||
void jetkvm_rpc_handler(const char *method, const char *params);
|
||||
|
||||
|
||||
// typedef void (jetkvm_video_state_handler_t)(jetkvm_video_state_t *state);
|
||||
// typedef void (jetkvm_log_handler_t)(int level, const char *filename, const char *funcname, int line, const char *message);
|
||||
// typedef void (jetkvm_rpc_handler_t)(const char *method, const char *params);
|
||||
// typedef void (jetkvm_video_handler_t)(const uint8_t *frame, ssize_t len);
|
||||
// typedef void (jetkvm_indev_handler_t)(int code);
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c
|
||||
index 2a4a034..760621a 100644
|
||||
--- a/internal/native/cgo/video.c
|
||||
+++ b/internal/native/cgo/video.c
|
||||
@@ -354,6 +354,10 @@ bool detected_signal = false, streaming_flag = false, streaming_stopped = true;
|
||||
pthread_t *streaming_thread = NULL;
|
||||
pthread_mutex_t streaming_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
+// Diagnostic tracking for validation
|
||||
+static uint64_t last_close_time = 0;
|
||||
+static int consecutive_failures = 0;
|
||||
+
|
||||
bool get_streaming_flag()
|
||||
{
|
||||
log_info("getting streaming flag");
|
||||
@@ -395,6 +399,12 @@ void *run_video_stream(void *arg)
|
||||
continue;
|
||||
}
|
||||
|
||||
+ // Log attempt to open with timing info
|
||||
+ RK_U64 time_since_close = last_close_time > 0 ? (get_us() - last_close_time) : 0;
|
||||
+ log_info("[DIAG] Attempting to open %s (time_since_last_close=%llu us)",
|
||||
+ VIDEO_DEV, time_since_close);
|
||||
+
|
||||
+ RK_U64 open_start_time = get_us();
|
||||
int video_dev_fd = open(VIDEO_DEV, O_RDWR);
|
||||
if (video_dev_fd < 0)
|
||||
{
|
||||
@@ -402,7 +412,9 @@ void *run_video_stream(void *arg)
|
||||
usleep(1000000);
|
||||
continue;
|
||||
}
|
||||
- log_info("opened video capture device %s", VIDEO_DEV);
|
||||
+ RK_U64 open_end_time = get_us();
|
||||
+ log_info("[DIAG] opened video capture device %s in %llu us",
|
||||
+ VIDEO_DEV, open_end_time - open_start_time);
|
||||
|
||||
uint32_t width = detected_width;
|
||||
uint32_t height = detected_height;
|
||||
@@ -414,14 +426,45 @@ void *run_video_stream(void *arg)
|
||||
fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_YUYV;
|
||||
fmt.fmt.pix_mp.field = V4L2_FIELD_ANY;
|
||||
|
||||
+ // Probe device state before attempting format set
|
||||
+ struct v4l2_format query_fmt;
|
||||
+ memset(&query_fmt, 0, sizeof(query_fmt));
|
||||
+ query_fmt.type = type;
|
||||
+ int query_ret = ioctl(video_dev_fd, VIDIOC_G_FMT, &query_fmt);
|
||||
+ log_info("[DIAG] VIDIOC_G_FMT probe: ret=%d, errno=%d (%s)",
|
||||
+ query_ret, query_ret < 0 ? errno : 0,
|
||||
+ query_ret < 0 ? strerror(errno) : "OK");
|
||||
+
|
||||
+ RK_U64 set_fmt_start_time = get_us();
|
||||
+ log_info("[DIAG] Attempting VIDIOC_S_FMT: %ux%u, time_since_open=%llu us",
|
||||
+ width, height, set_fmt_start_time - open_end_time);
|
||||
+
|
||||
if (ioctl(video_dev_fd, VIDIOC_S_FMT, &fmt) < 0)
|
||||
{
|
||||
- log_error("Set format fail: %s", strerror(errno));
|
||||
+ RK_U64 failure_time = get_us();
|
||||
+ int saved_errno = errno;
|
||||
+ consecutive_failures++;
|
||||
+
|
||||
+ log_error("[DIAG] Set format fail: errno=%d (%s)", saved_errno, strerror(saved_errno));
|
||||
+ log_error("[DIAG] Failure context: consecutive_failures=%d, time_since_open=%llu us, "
|
||||
+ "time_since_last_close=%llu us, resolution=%ux%u, streaming_flag=%d",
|
||||
+ consecutive_failures,
|
||||
+ failure_time - open_end_time,
|
||||
+ last_close_time > 0 ? (open_start_time - last_close_time) : 0,
|
||||
+ width, height,
|
||||
+ streaming_flag);
|
||||
+
|
||||
usleep(100000); // Sleep for 100 milliseconds
|
||||
close(video_dev_fd);
|
||||
+ last_close_time = get_us();
|
||||
+ log_info("[DIAG] Closed device after format failure at %llu us", last_close_time);
|
||||
continue;
|
||||
}
|
||||
|
||||
+ // Success - reset failure counter
|
||||
+ log_info("[DIAG] VIDIOC_S_FMT succeeded (previous consecutive failures: %d)", consecutive_failures);
|
||||
+ consecutive_failures = 0;
|
||||
+
|
||||
struct v4l2_buffer buf;
|
||||
|
||||
struct v4l2_requestbuffers req;
|
||||
@@ -601,9 +644,46 @@ void *run_video_stream(void *arg)
|
||||
}
|
||||
cleanup:
|
||||
log_info("cleaning up video capture device %s", VIDEO_DEV);
|
||||
- if (ioctl(video_dev_fd, VIDIOC_STREAMOFF, &type) < 0)
|
||||
+
|
||||
+ RK_U64 streamoff_start = get_us();
|
||||
+ log_info("[DIAG] Attempting VIDIOC_STREAMOFF");
|
||||
+
|
||||
+ int streamoff_ret = ioctl(video_dev_fd, VIDIOC_STREAMOFF, &type);
|
||||
+ RK_U64 streamoff_end = get_us();
|
||||
+
|
||||
+ if (streamoff_ret < 0)
|
||||
+ {
|
||||
+ log_error("[DIAG] VIDIOC_STREAMOFF failed: errno=%d (%s), duration=%llu us",
|
||||
+ errno, strerror(errno), streamoff_end - streamoff_start);
|
||||
+ }
|
||||
+ else
|
||||
+ {
|
||||
+ log_info("[DIAG] VIDIOC_STREAMOFF succeeded in %llu us",
|
||||
+ streamoff_end - streamoff_start);
|
||||
+ }
|
||||
+
|
||||
+ // VALIDATION TEST: Explicitly free V4L2 buffer queue
|
||||
+ struct v4l2_requestbuffers req_free;
|
||||
+ memset(&req_free, 0, sizeof(req_free));
|
||||
+ req_free.count = 0; // Tell driver to free all buffers
|
||||
+ req_free.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
||||
+ req_free.memory = V4L2_MEMORY_DMABUF;
|
||||
+
|
||||
+ RK_U64 reqbufs_start = get_us();
|
||||
+ log_info("[DIAG] VALIDATION: Calling VIDIOC_REQBUFS(count=0) to free buffer queue");
|
||||
+
|
||||
+ int reqbufs_ret = ioctl(video_dev_fd, VIDIOC_REQBUFS, &req_free);
|
||||
+ RK_U64 reqbufs_end = get_us();
|
||||
+
|
||||
+ if (reqbufs_ret < 0)
|
||||
+ {
|
||||
+ log_error("[DIAG] VALIDATION: REQBUFS(0) FAILED - errno=%d (%s), duration=%llu us",
|
||||
+ errno, strerror(errno), reqbufs_end - reqbufs_start);
|
||||
+ }
|
||||
+ else
|
||||
{
|
||||
- log_error("VIDIOC_STREAMOFF failed: %s", strerror(errno));
|
||||
+ log_info("[DIAG] VALIDATION: REQBUFS(0) SUCCEEDED - freed buffers in %llu us",
|
||||
+ reqbufs_end - reqbufs_start);
|
||||
}
|
||||
|
||||
venc_stop();
|
||||
@@ -617,9 +697,13 @@ void *run_video_stream(void *arg)
|
||||
}
|
||||
|
||||
log_info("closing video capture device %s", VIDEO_DEV);
|
||||
+ RK_U64 close_start = get_us();
|
||||
close(video_dev_fd);
|
||||
+ last_close_time = get_us();
|
||||
+ log_info("[DIAG] Device closed, took %llu us, timestamp=%llu",
|
||||
+ last_close_time - close_start, last_close_time);
|
||||
}
|
||||
-
|
||||
+
|
||||
log_info("video stream thread exiting");
|
||||
|
||||
streaming_stopped = true;
|
||||
@@ -648,7 +732,7 @@ void video_shutdown()
|
||||
RK_MPI_MB_DestroyPool(memPool);
|
||||
}
|
||||
log_info("Destroyed memory pool");
|
||||
-
|
||||
+
|
||||
pthread_mutex_destroy(&streaming_mutex);
|
||||
log_info("Destroyed streaming mutex");
|
||||
}
|
||||
@@ -665,14 +749,14 @@ void video_start_streaming()
|
||||
log_warn("video streaming already started");
|
||||
return;
|
||||
}
|
||||
-
|
||||
+
|
||||
pthread_t *new_thread = malloc(sizeof(pthread_t));
|
||||
if (new_thread == NULL)
|
||||
{
|
||||
log_error("Failed to allocate memory for streaming thread");
|
||||
return;
|
||||
}
|
||||
-
|
||||
+
|
||||
set_streaming_flag(true);
|
||||
int result = pthread_create(new_thread, NULL, run_video_stream, NULL);
|
||||
if (result != 0)
|
||||
@@ -682,7 +766,7 @@ void video_start_streaming()
|
||||
free(new_thread);
|
||||
return;
|
||||
}
|
||||
-
|
||||
+
|
||||
// Only set streaming_thread after successful creation
|
||||
streaming_thread = new_thread;
|
||||
}
|
||||
@@ -693,7 +777,7 @@ void video_stop_streaming()
|
||||
log_info("video streaming already stopped");
|
||||
return;
|
||||
}
|
||||
-
|
||||
+
|
||||
log_info("stopping video streaming");
|
||||
set_streaming_flag(false);
|
||||
|
||||
@@ -711,7 +795,7 @@ void video_stop_streaming()
|
||||
free(streaming_thread);
|
||||
streaming_thread = NULL;
|
||||
|
||||
- log_info("video streaming stopped");
|
||||
+ log_info("video streaming stopped");
|
||||
}
|
||||
|
||||
void video_restart_streaming()
|
||||
@@ -818,4 +902,4 @@ void video_set_quality_factor(float factor)
|
||||
|
||||
float video_get_quality_factor() {
|
||||
return quality_factor;
|
||||
-}
|
||||
\ No newline at end of file
|
||||
+}
|
||||
|
|
@ -50,17 +50,9 @@ static inline void jetkvm_cgo_setup_rpc_handler() {
|
|||
import "C"
|
||||
|
||||
var (
|
||||
cgoLock sync.Mutex
|
||||
cgoDisabled bool
|
||||
cgoLock sync.Mutex
|
||||
)
|
||||
|
||||
func setCgoDisabled(disabled bool) {
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
cgoDisabled = disabled
|
||||
}
|
||||
|
||||
//export jetkvm_go_video_state_handler
|
||||
func jetkvm_go_video_state_handler(state *C.jetkvm_video_state_t) {
|
||||
videoState := VideoState{
|
||||
|
|
@ -104,10 +96,6 @@ func jetkvm_go_rpc_handler(method *C.cchar_t, params *C.cchar_t) {
|
|||
var eventCodeToNameMap = map[int]string{}
|
||||
|
||||
func uiEventCodeToName(code int) string {
|
||||
if cgoDisabled {
|
||||
return ""
|
||||
}
|
||||
|
||||
name, ok := eventCodeToNameMap[code]
|
||||
if !ok {
|
||||
cCode := C.int(code)
|
||||
|
|
@ -120,10 +108,6 @@ func uiEventCodeToName(code int) string {
|
|||
}
|
||||
|
||||
func setUpNativeHandlers() {
|
||||
if cgoDisabled {
|
||||
return
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -135,10 +119,6 @@ func setUpNativeHandlers() {
|
|||
}
|
||||
|
||||
func uiInit(rotation uint16) {
|
||||
if cgoDisabled {
|
||||
return
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -148,10 +128,6 @@ func uiInit(rotation uint16) {
|
|||
}
|
||||
|
||||
func uiTick() {
|
||||
if cgoDisabled {
|
||||
return
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -159,10 +135,6 @@ func uiTick() {
|
|||
}
|
||||
|
||||
func videoInit(factor float64) error {
|
||||
if cgoDisabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -176,10 +148,6 @@ func videoInit(factor float64) error {
|
|||
}
|
||||
|
||||
func videoShutdown() {
|
||||
if cgoDisabled {
|
||||
return
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -187,10 +155,6 @@ func videoShutdown() {
|
|||
}
|
||||
|
||||
func videoStart() {
|
||||
if cgoDisabled {
|
||||
return
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -198,10 +162,6 @@ func videoStart() {
|
|||
}
|
||||
|
||||
func videoStop() {
|
||||
if cgoDisabled {
|
||||
return
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -209,10 +169,6 @@ func videoStop() {
|
|||
}
|
||||
|
||||
func videoLogStatus() string {
|
||||
if cgoDisabled {
|
||||
return ""
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -223,10 +179,6 @@ func videoLogStatus() string {
|
|||
}
|
||||
|
||||
func uiSetVar(name string, value string) {
|
||||
if cgoDisabled {
|
||||
return
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -240,10 +192,6 @@ func uiSetVar(name string, value string) {
|
|||
}
|
||||
|
||||
func uiGetVar(name string) string {
|
||||
if cgoDisabled {
|
||||
return ""
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -254,10 +202,6 @@ func uiGetVar(name string) string {
|
|||
}
|
||||
|
||||
func uiSwitchToScreen(screen string) {
|
||||
if cgoDisabled {
|
||||
return
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -267,10 +211,6 @@ func uiSwitchToScreen(screen string) {
|
|||
}
|
||||
|
||||
func uiGetCurrentScreen() string {
|
||||
if cgoDisabled {
|
||||
return ""
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -279,10 +219,6 @@ func uiGetCurrentScreen() string {
|
|||
}
|
||||
|
||||
func uiObjAddState(objName string, state string) (bool, error) {
|
||||
if cgoDisabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -295,10 +231,6 @@ func uiObjAddState(objName string, state string) (bool, error) {
|
|||
}
|
||||
|
||||
func uiObjClearState(objName string, state string) (bool, error) {
|
||||
if cgoDisabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -311,10 +243,6 @@ func uiObjClearState(objName string, state string) (bool, error) {
|
|||
}
|
||||
|
||||
func uiGetLVGLVersion() string {
|
||||
if cgoDisabled {
|
||||
return ""
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -323,10 +251,6 @@ func uiGetLVGLVersion() string {
|
|||
|
||||
// TODO: use Enum instead of string but it's not a hot path and performance is not a concern now
|
||||
func uiObjAddFlag(objName string, flag string) (bool, error) {
|
||||
if cgoDisabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -339,10 +263,6 @@ func uiObjAddFlag(objName string, flag string) (bool, error) {
|
|||
}
|
||||
|
||||
func uiObjClearFlag(objName string, flag string) (bool, error) {
|
||||
if cgoDisabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -363,10 +283,6 @@ func uiObjShow(objName string) (bool, error) {
|
|||
}
|
||||
|
||||
func uiObjSetOpacity(objName string, opacity int) (bool, error) {
|
||||
if cgoDisabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -378,10 +294,6 @@ func uiObjSetOpacity(objName string, opacity int) (bool, error) {
|
|||
}
|
||||
|
||||
func uiObjFadeIn(objName string, duration uint32) (bool, error) {
|
||||
if cgoDisabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -394,10 +306,6 @@ func uiObjFadeIn(objName string, duration uint32) (bool, error) {
|
|||
}
|
||||
|
||||
func uiObjFadeOut(objName string, duration uint32) (bool, error) {
|
||||
if cgoDisabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -410,10 +318,6 @@ func uiObjFadeOut(objName string, duration uint32) (bool, error) {
|
|||
}
|
||||
|
||||
func uiLabelSetText(objName string, text string) (bool, error) {
|
||||
if cgoDisabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -431,10 +335,6 @@ func uiLabelSetText(objName string, text string) (bool, error) {
|
|||
}
|
||||
|
||||
func uiImgSetSrc(objName string, src string) (bool, error) {
|
||||
if cgoDisabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -450,10 +350,6 @@ func uiImgSetSrc(objName string, src string) (bool, error) {
|
|||
}
|
||||
|
||||
func uiDispSetRotation(rotation uint16) (bool, error) {
|
||||
if cgoDisabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -466,10 +362,6 @@ func uiDispSetRotation(rotation uint16) (bool, error) {
|
|||
}
|
||||
|
||||
func videoGetStreamQualityFactor() (float64, error) {
|
||||
if cgoDisabled {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -478,10 +370,6 @@ func videoGetStreamQualityFactor() (float64, error) {
|
|||
}
|
||||
|
||||
func videoSetStreamQualityFactor(factor float64) error {
|
||||
if cgoDisabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -490,10 +378,6 @@ func videoSetStreamQualityFactor(factor float64) error {
|
|||
}
|
||||
|
||||
func videoGetEDID() (string, error) {
|
||||
if cgoDisabled {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
@ -502,10 +386,6 @@ func videoGetEDID() (string, error) {
|
|||
}
|
||||
|
||||
func videoSetEDID(edid string) error {
|
||||
if cgoDisabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
cgoLock.Lock()
|
||||
defer cgoLock.Unlock()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
package native
|
||||
|
||||
type EmptyNativeInterface struct {
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) Start() error { return nil }
|
||||
|
||||
func (e *EmptyNativeInterface) VideoSetSleepMode(enabled bool) error { return nil }
|
||||
|
||||
func (e *EmptyNativeInterface) VideoGetSleepMode() (bool, error) { return false, nil }
|
||||
|
||||
func (e *EmptyNativeInterface) VideoSleepModeSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) VideoSetQualityFactor(factor float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) VideoGetQualityFactor() (float64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) VideoSetEDID(edid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) VideoGetEDID() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) VideoLogStatus() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) VideoStop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) VideoStart() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) GetLVGLVersion() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIObjHide(objName string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIObjShow(objName string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UISetVar(name string, value string) {
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIGetVar(name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIObjAddState(objName string, state string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIObjClearState(objName string, state string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIObjAddFlag(objName string, flag string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIObjClearFlag(objName string, flag string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIObjSetOpacity(objName string, opacity int) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIObjFadeIn(objName string, duration uint32) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIObjFadeOut(objName string, duration uint32) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIObjSetLabelText(objName string, text string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UIObjSetImageSrc(objName string, image string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) DisplaySetRotation(rotation uint16) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *EmptyNativeInterface) UpdateLabelIfChanged(objName string, newText string) {}
|
||||
|
||||
func (e *EmptyNativeInterface) UpdateLabelAndChangeVisibility(objName string, newText string) {}
|
||||
|
||||
func (e *EmptyNativeInterface) SwitchToScreenIf(screenName string, shouldSwitch []string) {}
|
||||
|
||||
func (e *EmptyNativeInterface) SwitchToScreenIfDifferent(screenName string) {}
|
||||
|
||||
func (e *EmptyNativeInterface) DoNotUseThisIsForCrashTestingOnly() {}
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
pb "github.com/jetkvm/kvm/internal/native/proto"
|
||||
)
|
||||
|
||||
// GRPCClient wraps the gRPC client for the native service
|
||||
type GRPCClient struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
conn *grpc.ClientConn
|
||||
client pb.NativeServiceClient
|
||||
logger *zerolog.Logger
|
||||
|
||||
eventStream pb.NativeService_StreamEventsClient
|
||||
eventM sync.RWMutex
|
||||
eventCh chan *pb.Event
|
||||
eventDone chan struct{}
|
||||
|
||||
onVideoStateChange func(state VideoState)
|
||||
onIndevEvent func(event string)
|
||||
onRpcEvent func(event string)
|
||||
|
||||
closed bool
|
||||
closeM sync.Mutex
|
||||
}
|
||||
|
||||
type grpcClientOptions struct {
|
||||
SocketPath string
|
||||
Logger *zerolog.Logger
|
||||
OnVideoStateChange func(state VideoState)
|
||||
OnIndevEvent func(event string)
|
||||
OnRpcEvent func(event string)
|
||||
}
|
||||
|
||||
// NewGRPCClient creates a new gRPC client connected to the native service
|
||||
func NewGRPCClient(opts grpcClientOptions) (*GRPCClient, error) {
|
||||
// Connect to the Unix domain socket
|
||||
conn, err := grpc.NewClient(
|
||||
fmt.Sprintf("unix-abstract:%v", opts.SocketPath),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to gRPC server: %w", err)
|
||||
}
|
||||
|
||||
client := pb.NewNativeServiceClient(conn)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
grpcClient := &GRPCClient{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
conn: conn,
|
||||
client: client,
|
||||
logger: opts.Logger,
|
||||
eventCh: make(chan *pb.Event, 100),
|
||||
eventDone: make(chan struct{}),
|
||||
onVideoStateChange: opts.OnVideoStateChange,
|
||||
onIndevEvent: opts.OnIndevEvent,
|
||||
onRpcEvent: opts.OnRpcEvent,
|
||||
}
|
||||
|
||||
// Start event stream
|
||||
go grpcClient.startEventStream()
|
||||
|
||||
return grpcClient, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) handleEventStream(stream pb.NativeService_StreamEventsClient) {
|
||||
c.eventM.Lock()
|
||||
c.eventStream = stream
|
||||
defer func() {
|
||||
c.eventStream = nil
|
||||
c.eventM.Unlock()
|
||||
}()
|
||||
|
||||
for {
|
||||
logger := c.logger.With().Interface("stream", stream).Logger()
|
||||
if stream == nil {
|
||||
logger.Error().Msg("event stream is nil")
|
||||
break
|
||||
}
|
||||
|
||||
event, err := stream.Recv()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
logger.Debug().Msg("event stream closed")
|
||||
} else {
|
||||
logger.Warn().Err(err).Msg("event stream error")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// enrich the logger with the event type and data, if debug mode is enabled
|
||||
if c.logger.GetLevel() <= zerolog.DebugLevel {
|
||||
logger = logger.With().
|
||||
Str("type", event.Type).
|
||||
Interface("data", event.Data).
|
||||
Logger()
|
||||
}
|
||||
logger.Trace().Msg("received event")
|
||||
|
||||
select {
|
||||
case c.eventCh <- event:
|
||||
default:
|
||||
logger.Warn().Msg("event channel full, dropping event")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GRPCClient) startEventStream() {
|
||||
for {
|
||||
// check if the client is closed
|
||||
c.closeM.Lock()
|
||||
if c.closed {
|
||||
c.closeM.Unlock()
|
||||
return
|
||||
}
|
||||
c.closeM.Unlock()
|
||||
|
||||
// check if the context is done
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
c.logger.Info().Msg("event stream context done, closing")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
stream, err := c.client.StreamEvents(c.ctx, &pb.Empty{})
|
||||
if err != nil {
|
||||
c.logger.Warn().Err(err).Msg("failed to start event stream, retrying ...")
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
c.handleEventStream(stream)
|
||||
|
||||
// Wait before retrying
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GRPCClient) checkIsReady(ctx context.Context) error {
|
||||
c.logger.Trace().Msg("connection is idle, connecting ...")
|
||||
|
||||
resp, err := c.client.IsReady(ctx, &pb.IsReadyRequest{})
|
||||
if err != nil {
|
||||
if errors.Is(err, status.Error(codes.Unavailable, "")) {
|
||||
return fmt.Errorf("timeout waiting for ready: %w", err)
|
||||
}
|
||||
return fmt.Errorf("failed to check if ready: %w", err)
|
||||
}
|
||||
if resp.Ready {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitReady waits for the gRPC connection to be ready
|
||||
func (c *GRPCClient) WaitReady() error {
|
||||
ctx, cancel := context.WithTimeout(c.ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
prevState := connectivity.Idle
|
||||
for {
|
||||
state := c.conn.GetState()
|
||||
c.logger.
|
||||
With().
|
||||
Str("state", state.String()).
|
||||
Int("prev_state", int(prevState)).
|
||||
Logger()
|
||||
|
||||
prevState = state
|
||||
if state == connectivity.Idle || state == connectivity.Ready {
|
||||
if err := c.checkIsReady(ctx); err != nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
c.logger.Info().Msg("waiting for connection to be ready")
|
||||
|
||||
if state == connectivity.Ready {
|
||||
return nil
|
||||
}
|
||||
if state == connectivity.Shutdown {
|
||||
return fmt.Errorf("connection failed: %v", state)
|
||||
}
|
||||
|
||||
if !c.conn.WaitForStateChange(ctx, state) {
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GRPCClient) handleEvent(event *pb.Event) {
|
||||
switch event.Type {
|
||||
case "video_state_change":
|
||||
state := event.GetVideoState()
|
||||
if state == nil {
|
||||
c.logger.Warn().Msg("video state event is nil")
|
||||
return
|
||||
}
|
||||
c.onVideoStateChange(VideoState{
|
||||
Ready: state.Ready,
|
||||
Error: state.Error,
|
||||
Width: int(state.Width),
|
||||
Height: int(state.Height),
|
||||
FramePerSecond: state.FramePerSecond,
|
||||
})
|
||||
case "indev_event":
|
||||
c.onIndevEvent(event.GetIndevEvent())
|
||||
case "rpc_event":
|
||||
c.onRpcEvent(event.GetRpcEvent())
|
||||
default:
|
||||
c.logger.Warn().Str("type", event.Type).Msg("unknown event type")
|
||||
}
|
||||
}
|
||||
|
||||
// OnEvent registers an event handler
|
||||
func (c *GRPCClient) OnEvent(eventType string, handler func(data interface{})) {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event := <-c.eventCh:
|
||||
c.handleEvent(event)
|
||||
case <-c.eventDone:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Close closes the gRPC client
|
||||
func (c *GRPCClient) Close() error {
|
||||
c.closeM.Lock()
|
||||
defer c.closeM.Unlock()
|
||||
if c.closed {
|
||||
return nil
|
||||
}
|
||||
c.closed = true
|
||||
|
||||
// cancel all ongoing operations
|
||||
c.cancel()
|
||||
|
||||
close(c.eventDone)
|
||||
|
||||
c.eventM.Lock()
|
||||
if c.eventStream != nil {
|
||||
if err := c.eventStream.CloseSend(); err != nil {
|
||||
c.logger.Warn().Err(err).Msg("failed to close event stream")
|
||||
}
|
||||
}
|
||||
c.eventM.Unlock()
|
||||
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
pb "github.com/jetkvm/kvm/internal/native/proto"
|
||||
)
|
||||
|
||||
// Below are generated methods, do not edit manually
|
||||
|
||||
// Video methods
|
||||
func (c *GRPCClient) VideoSetSleepMode(enabled bool) error {
|
||||
_, err := c.client.VideoSetSleepMode(context.Background(), &pb.VideoSetSleepModeRequest{Enabled: enabled})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *GRPCClient) VideoGetSleepMode() (bool, error) {
|
||||
resp, err := c.client.VideoGetSleepMode(context.Background(), &pb.Empty{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Enabled, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) VideoSleepModeSupported() bool {
|
||||
resp, err := c.client.VideoSleepModeSupported(context.Background(), &pb.Empty{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return resp.Supported
|
||||
}
|
||||
|
||||
func (c *GRPCClient) VideoSetQualityFactor(factor float64) error {
|
||||
_, err := c.client.VideoSetQualityFactor(context.Background(), &pb.VideoSetQualityFactorRequest{Factor: factor})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *GRPCClient) VideoGetQualityFactor() (float64, error) {
|
||||
resp, err := c.client.VideoGetQualityFactor(context.Background(), &pb.Empty{})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return resp.Factor, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) VideoSetEDID(edid string) error {
|
||||
_, err := c.client.VideoSetEDID(context.Background(), &pb.VideoSetEDIDRequest{Edid: edid})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *GRPCClient) VideoGetEDID() (string, error) {
|
||||
resp, err := c.client.VideoGetEDID(context.Background(), &pb.Empty{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Edid, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) VideoLogStatus() (string, error) {
|
||||
resp, err := c.client.VideoLogStatus(context.Background(), &pb.Empty{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Status, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) VideoStop() error {
|
||||
_, err := c.client.VideoStop(context.Background(), &pb.Empty{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *GRPCClient) VideoStart() error {
|
||||
_, err := c.client.VideoStart(context.Background(), &pb.Empty{})
|
||||
return err
|
||||
}
|
||||
|
||||
// UI methods
|
||||
func (c *GRPCClient) GetLVGLVersion() (string, error) {
|
||||
resp, err := c.client.GetLVGLVersion(context.Background(), &pb.Empty{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Version, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIObjHide(objName string) (bool, error) {
|
||||
resp, err := c.client.UIObjHide(context.Background(), &pb.UIObjHideRequest{ObjName: objName})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIObjShow(objName string) (bool, error) {
|
||||
resp, err := c.client.UIObjShow(context.Background(), &pb.UIObjShowRequest{ObjName: objName})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UISetVar(name string, value string) {
|
||||
_, _ = c.client.UISetVar(context.Background(), &pb.UISetVarRequest{Name: name, Value: value})
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIGetVar(name string) string {
|
||||
resp, err := c.client.UIGetVar(context.Background(), &pb.UIGetVarRequest{Name: name})
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return resp.Value
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIObjAddState(objName string, state string) (bool, error) {
|
||||
resp, err := c.client.UIObjAddState(context.Background(), &pb.UIObjAddStateRequest{ObjName: objName, State: state})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIObjClearState(objName string, state string) (bool, error) {
|
||||
resp, err := c.client.UIObjClearState(context.Background(), &pb.UIObjClearStateRequest{ObjName: objName, State: state})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIObjAddFlag(objName string, flag string) (bool, error) {
|
||||
resp, err := c.client.UIObjAddFlag(context.Background(), &pb.UIObjAddFlagRequest{ObjName: objName, Flag: flag})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIObjClearFlag(objName string, flag string) (bool, error) {
|
||||
resp, err := c.client.UIObjClearFlag(context.Background(), &pb.UIObjClearFlagRequest{ObjName: objName, Flag: flag})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIObjSetOpacity(objName string, opacity int) (bool, error) {
|
||||
resp, err := c.client.UIObjSetOpacity(context.Background(), &pb.UIObjSetOpacityRequest{ObjName: objName, Opacity: int32(opacity)})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIObjFadeIn(objName string, duration uint32) (bool, error) {
|
||||
resp, err := c.client.UIObjFadeIn(context.Background(), &pb.UIObjFadeInRequest{ObjName: objName, Duration: duration})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIObjFadeOut(objName string, duration uint32) (bool, error) {
|
||||
resp, err := c.client.UIObjFadeOut(context.Background(), &pb.UIObjFadeOutRequest{ObjName: objName, Duration: duration})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIObjSetLabelText(objName string, text string) (bool, error) {
|
||||
resp, err := c.client.UIObjSetLabelText(context.Background(), &pb.UIObjSetLabelTextRequest{ObjName: objName, Text: text})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UIObjSetImageSrc(objName string, image string) (bool, error) {
|
||||
resp, err := c.client.UIObjSetImageSrc(context.Background(), &pb.UIObjSetImageSrcRequest{ObjName: objName, Image: image})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) DisplaySetRotation(rotation uint16) (bool, error) {
|
||||
resp, err := c.client.DisplaySetRotation(context.Background(), &pb.DisplaySetRotationRequest{Rotation: uint32(rotation)})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.Success, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UpdateLabelIfChanged(objName string, newText string) {
|
||||
_, _ = c.client.UpdateLabelIfChanged(context.Background(), &pb.UpdateLabelIfChangedRequest{ObjName: objName, NewText: newText})
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UpdateLabelAndChangeVisibility(objName string, newText string) {
|
||||
_, _ = c.client.UpdateLabelAndChangeVisibility(context.Background(), &pb.UpdateLabelAndChangeVisibilityRequest{ObjName: objName, NewText: newText})
|
||||
}
|
||||
|
||||
func (c *GRPCClient) SwitchToScreenIf(screenName string, shouldSwitch []string) {
|
||||
_, _ = c.client.SwitchToScreenIf(context.Background(), &pb.SwitchToScreenIfRequest{ScreenName: screenName, ShouldSwitch: shouldSwitch})
|
||||
}
|
||||
|
||||
func (c *GRPCClient) SwitchToScreenIfDifferent(screenName string) {
|
||||
_, _ = c.client.SwitchToScreenIfDifferent(context.Background(), &pb.SwitchToScreenIfDifferentRequest{ScreenName: screenName})
|
||||
}
|
||||
|
||||
func (c *GRPCClient) DoNotUseThisIsForCrashTestingOnly() {
|
||||
_, _ = c.client.DoNotUseThisIsForCrashTestingOnly(context.Background(), &pb.Empty{})
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
pb "github.com/jetkvm/kvm/internal/native/proto"
|
||||
)
|
||||
|
||||
// grpcServer wraps the Native instance and implements the gRPC service
|
||||
type grpcServer struct {
|
||||
pb.UnimplementedNativeServiceServer
|
||||
native *Native
|
||||
logger *zerolog.Logger
|
||||
eventChs []chan *pb.Event
|
||||
eventM sync.Mutex
|
||||
}
|
||||
|
||||
// NewGRPCServer creates a new gRPC server for the native service
|
||||
func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
|
||||
s := &grpcServer{
|
||||
native: n,
|
||||
logger: logger,
|
||||
eventChs: make([]chan *pb.Event, 0),
|
||||
}
|
||||
|
||||
// Store original callbacks and wrap them to also broadcast events
|
||||
originalVideoStateChange := n.onVideoStateChange
|
||||
originalIndevEvent := n.onIndevEvent
|
||||
originalRpcEvent := n.onRpcEvent
|
||||
|
||||
// Wrap callbacks to both call original and broadcast events
|
||||
n.onVideoStateChange = func(state VideoState) {
|
||||
if originalVideoStateChange != nil {
|
||||
originalVideoStateChange(state)
|
||||
}
|
||||
event := &pb.Event{
|
||||
Type: "video_state_change",
|
||||
Data: &pb.Event_VideoState{
|
||||
VideoState: &pb.VideoState{
|
||||
Ready: state.Ready,
|
||||
Error: state.Error,
|
||||
Width: int32(state.Width),
|
||||
Height: int32(state.Height),
|
||||
FramePerSecond: state.FramePerSecond,
|
||||
},
|
||||
},
|
||||
}
|
||||
s.broadcastEvent(event)
|
||||
}
|
||||
|
||||
n.onIndevEvent = func(event string) {
|
||||
if originalIndevEvent != nil {
|
||||
originalIndevEvent(event)
|
||||
}
|
||||
s.broadcastEvent(&pb.Event{
|
||||
Type: "indev_event",
|
||||
Data: &pb.Event_IndevEvent{
|
||||
IndevEvent: event,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
n.onRpcEvent = func(event string) {
|
||||
if originalRpcEvent != nil {
|
||||
originalRpcEvent(event)
|
||||
}
|
||||
s.broadcastEvent(&pb.Event{
|
||||
Type: "rpc_event",
|
||||
Data: &pb.Event_RpcEvent{
|
||||
RpcEvent: event,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *grpcServer) broadcastEvent(event *pb.Event) {
|
||||
s.eventM.Lock()
|
||||
defer s.eventM.Unlock()
|
||||
|
||||
for _, ch := range s.eventChs {
|
||||
select {
|
||||
case ch <- event:
|
||||
default:
|
||||
// Channel full, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *grpcServer) IsReady(ctx context.Context, req *pb.IsReadyRequest) (*pb.IsReadyResponse, error) {
|
||||
return &pb.IsReadyResponse{Ready: true, VideoReady: true}, nil
|
||||
}
|
||||
|
||||
// StreamEvents streams events from the native process
|
||||
func (s *grpcServer) StreamEvents(req *pb.Empty, stream pb.NativeService_StreamEventsServer) error {
|
||||
setProcTitle("connected")
|
||||
defer setProcTitle("waiting")
|
||||
|
||||
eventCh := make(chan *pb.Event, 100)
|
||||
|
||||
// Register this channel for events
|
||||
s.eventM.Lock()
|
||||
s.eventChs = append(s.eventChs, eventCh)
|
||||
s.eventM.Unlock()
|
||||
|
||||
// Unregister on exit
|
||||
defer func() {
|
||||
s.eventM.Lock()
|
||||
defer s.eventM.Unlock()
|
||||
for i, ch := range s.eventChs {
|
||||
if ch == eventCh {
|
||||
s.eventChs = append(s.eventChs[:i], s.eventChs[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
close(eventCh)
|
||||
}()
|
||||
|
||||
// Stream events
|
||||
for {
|
||||
select {
|
||||
case event := <-eventCh:
|
||||
if err := stream.Send(event); err != nil {
|
||||
return err
|
||||
}
|
||||
case <-stream.Context().Done():
|
||||
return stream.Context().Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StartGRPCServer starts the gRPC server on a Unix domain socket
|
||||
func StartGRPCServer(server *grpcServer, socketPath string, logger *zerolog.Logger) (*grpc.Server, net.Listener, error) {
|
||||
lis, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to listen on socket: %w", err)
|
||||
}
|
||||
|
||||
s := grpc.NewServer()
|
||||
pb.RegisterNativeServiceServer(s, server)
|
||||
|
||||
go func() {
|
||||
if err := s.Serve(lis); err != nil {
|
||||
logger.Error().Err(err).Msg("gRPC server error")
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Info().Str("socket", socketPath).Msg("gRPC server started")
|
||||
return s, lis, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
pb "github.com/jetkvm/kvm/internal/native/proto"
|
||||
)
|
||||
|
||||
// Below are generated methods, do not edit manually
|
||||
|
||||
// Video methods
|
||||
func (s *grpcServer) VideoSetSleepMode(ctx context.Context, req *pb.VideoSetSleepModeRequest) (*pb.Empty, error) {
|
||||
if err := s.native.VideoSetSleepMode(req.Enabled); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) VideoGetSleepMode(ctx context.Context, req *pb.Empty) (*pb.VideoGetSleepModeResponse, error) {
|
||||
enabled, err := s.native.VideoGetSleepMode()
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.VideoGetSleepModeResponse{Enabled: enabled}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) VideoSleepModeSupported(ctx context.Context, req *pb.Empty) (*pb.VideoSleepModeSupportedResponse, error) {
|
||||
return &pb.VideoSleepModeSupportedResponse{Supported: s.native.VideoSleepModeSupported()}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) VideoSetQualityFactor(ctx context.Context, req *pb.VideoSetQualityFactorRequest) (*pb.Empty, error) {
|
||||
if err := s.native.VideoSetQualityFactor(req.Factor); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) VideoGetQualityFactor(ctx context.Context, req *pb.Empty) (*pb.VideoGetQualityFactorResponse, error) {
|
||||
factor, err := s.native.VideoGetQualityFactor()
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.VideoGetQualityFactorResponse{Factor: factor}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) VideoSetEDID(ctx context.Context, req *pb.VideoSetEDIDRequest) (*pb.Empty, error) {
|
||||
if err := s.native.VideoSetEDID(req.Edid); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) VideoGetEDID(ctx context.Context, req *pb.Empty) (*pb.VideoGetEDIDResponse, error) {
|
||||
edid, err := s.native.VideoGetEDID()
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.VideoGetEDIDResponse{Edid: edid}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) VideoLogStatus(ctx context.Context, req *pb.Empty) (*pb.VideoLogStatusResponse, error) {
|
||||
logStatus, err := s.native.VideoLogStatus()
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.VideoLogStatusResponse{Status: logStatus}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) VideoStop(ctx context.Context, req *pb.Empty) (*pb.Empty, error) {
|
||||
procPrefix = "jetkvm: [native]"
|
||||
setProcTitle(lastProcTitle)
|
||||
|
||||
if err := s.native.VideoStop(); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) VideoStart(ctx context.Context, req *pb.Empty) (*pb.Empty, error) {
|
||||
procPrefix = "jetkvm: [native+video]"
|
||||
setProcTitle(lastProcTitle)
|
||||
|
||||
if err := s.native.VideoStart(); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
// UI methods
|
||||
func (s *grpcServer) GetLVGLVersion(ctx context.Context, req *pb.Empty) (*pb.GetLVGLVersionResponse, error) {
|
||||
version, err := s.native.GetLVGLVersion()
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.GetLVGLVersionResponse{Version: version}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIObjHide(ctx context.Context, req *pb.UIObjHideRequest) (*pb.UIObjHideResponse, error) {
|
||||
success, err := s.native.UIObjHide(req.ObjName)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.UIObjHideResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIObjShow(ctx context.Context, req *pb.UIObjShowRequest) (*pb.UIObjShowResponse, error) {
|
||||
success, err := s.native.UIObjShow(req.ObjName)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.UIObjShowResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UISetVar(ctx context.Context, req *pb.UISetVarRequest) (*pb.Empty, error) {
|
||||
s.native.UISetVar(req.Name, req.Value)
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIGetVar(ctx context.Context, req *pb.UIGetVarRequest) (*pb.UIGetVarResponse, error) {
|
||||
value := s.native.UIGetVar(req.Name)
|
||||
return &pb.UIGetVarResponse{Value: value}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIObjAddState(ctx context.Context, req *pb.UIObjAddStateRequest) (*pb.UIObjAddStateResponse, error) {
|
||||
success, err := s.native.UIObjAddState(req.ObjName, req.State)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.UIObjAddStateResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIObjClearState(ctx context.Context, req *pb.UIObjClearStateRequest) (*pb.UIObjClearStateResponse, error) {
|
||||
success, err := s.native.UIObjClearState(req.ObjName, req.State)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.UIObjClearStateResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIObjAddFlag(ctx context.Context, req *pb.UIObjAddFlagRequest) (*pb.UIObjAddFlagResponse, error) {
|
||||
success, err := s.native.UIObjAddFlag(req.ObjName, req.Flag)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.UIObjAddFlagResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIObjClearFlag(ctx context.Context, req *pb.UIObjClearFlagRequest) (*pb.UIObjClearFlagResponse, error) {
|
||||
success, err := s.native.UIObjClearFlag(req.ObjName, req.Flag)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.UIObjClearFlagResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIObjSetOpacity(ctx context.Context, req *pb.UIObjSetOpacityRequest) (*pb.UIObjSetOpacityResponse, error) {
|
||||
success, err := s.native.UIObjSetOpacity(req.ObjName, int(req.Opacity))
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.UIObjSetOpacityResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIObjFadeIn(ctx context.Context, req *pb.UIObjFadeInRequest) (*pb.UIObjFadeInResponse, error) {
|
||||
success, err := s.native.UIObjFadeIn(req.ObjName, req.Duration)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.UIObjFadeInResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIObjFadeOut(ctx context.Context, req *pb.UIObjFadeOutRequest) (*pb.UIObjFadeOutResponse, error) {
|
||||
success, err := s.native.UIObjFadeOut(req.ObjName, req.Duration)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.UIObjFadeOutResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIObjSetLabelText(ctx context.Context, req *pb.UIObjSetLabelTextRequest) (*pb.UIObjSetLabelTextResponse, error) {
|
||||
success, err := s.native.UIObjSetLabelText(req.ObjName, req.Text)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.UIObjSetLabelTextResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UIObjSetImageSrc(ctx context.Context, req *pb.UIObjSetImageSrcRequest) (*pb.UIObjSetImageSrcResponse, error) {
|
||||
success, err := s.native.UIObjSetImageSrc(req.ObjName, req.Image)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.UIObjSetImageSrcResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) DisplaySetRotation(ctx context.Context, req *pb.DisplaySetRotationRequest) (*pb.DisplaySetRotationResponse, error) {
|
||||
success, err := s.native.DisplaySetRotation(uint16(req.Rotation))
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return &pb.DisplaySetRotationResponse{Success: success}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UpdateLabelIfChanged(ctx context.Context, req *pb.UpdateLabelIfChangedRequest) (*pb.Empty, error) {
|
||||
s.native.UpdateLabelIfChanged(req.ObjName, req.NewText)
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) UpdateLabelAndChangeVisibility(ctx context.Context, req *pb.UpdateLabelAndChangeVisibilityRequest) (*pb.Empty, error) {
|
||||
s.native.UpdateLabelAndChangeVisibility(req.ObjName, req.NewText)
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) SwitchToScreenIf(ctx context.Context, req *pb.SwitchToScreenIfRequest) (*pb.Empty, error) {
|
||||
s.native.SwitchToScreenIf(req.ScreenName, req.ShouldSwitch)
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) SwitchToScreenIfDifferent(ctx context.Context, req *pb.SwitchToScreenIfDifferentRequest) (*pb.Empty, error) {
|
||||
s.native.SwitchToScreenIfDifferent(req.ScreenName)
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) DoNotUseThisIsForCrashTestingOnly(ctx context.Context, req *pb.Empty) (*pb.Empty, error) {
|
||||
s.native.DoNotUseThisIsForCrashTestingOnly()
|
||||
return &pb.Empty{}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package native
|
||||
|
||||
// NativeInterface defines the interface that both Native and NativeProxy implement
|
||||
type NativeInterface interface {
|
||||
Start() error
|
||||
VideoSetSleepMode(enabled bool) error
|
||||
VideoGetSleepMode() (bool, error)
|
||||
VideoSleepModeSupported() bool
|
||||
VideoSetQualityFactor(factor float64) error
|
||||
VideoGetQualityFactor() (float64, error)
|
||||
VideoSetEDID(edid string) error
|
||||
VideoGetEDID() (string, error)
|
||||
VideoLogStatus() (string, error)
|
||||
VideoStop() error
|
||||
VideoStart() error
|
||||
GetLVGLVersion() (string, error)
|
||||
UIObjHide(objName string) (bool, error)
|
||||
UIObjShow(objName string) (bool, error)
|
||||
UISetVar(name string, value string)
|
||||
UIGetVar(name string) string
|
||||
UIObjAddState(objName string, state string) (bool, error)
|
||||
UIObjClearState(objName string, state string) (bool, error)
|
||||
UIObjAddFlag(objName string, flag string) (bool, error)
|
||||
UIObjClearFlag(objName string, flag string) (bool, error)
|
||||
UIObjSetOpacity(objName string, opacity int) (bool, error)
|
||||
UIObjFadeIn(objName string, duration uint32) (bool, error)
|
||||
UIObjFadeOut(objName string, duration uint32) (bool, error)
|
||||
UIObjSetLabelText(objName string, text string) (bool, error)
|
||||
UIObjSetImageSrc(objName string, image string) (bool, error)
|
||||
DisplaySetRotation(rotation uint16) (bool, error)
|
||||
UpdateLabelIfChanged(objName string, newText string)
|
||||
UpdateLabelAndChangeVisibility(objName string, newText string)
|
||||
SwitchToScreenIf(screenName string, shouldSwitch []string)
|
||||
SwitchToScreenIfDifferent(screenName string)
|
||||
DoNotUseThisIsForCrashTestingOnly()
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -9,7 +10,6 @@ import (
|
|||
)
|
||||
|
||||
type Native struct {
|
||||
disable bool
|
||||
ready chan struct{}
|
||||
l *zerolog.Logger
|
||||
lD *zerolog.Logger
|
||||
|
|
@ -28,18 +28,23 @@ type Native struct {
|
|||
}
|
||||
|
||||
type NativeOptions struct {
|
||||
Disable bool
|
||||
SystemVersion *semver.Version
|
||||
AppVersion *semver.Version
|
||||
DisplayRotation uint16
|
||||
DefaultQualityFactor float64
|
||||
MaxRestartAttempts uint
|
||||
OnVideoStateChange func(state VideoState)
|
||||
OnVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||
OnIndevEvent func(event string)
|
||||
OnRpcEvent func(event string)
|
||||
OnNativeRestart func()
|
||||
}
|
||||
|
||||
func NewNative(opts NativeOptions) *Native {
|
||||
pid := os.Getpid()
|
||||
nativeSubLogger := nativeLogger.With().Int("pid", pid).Str("scope", "native").Logger()
|
||||
displaySubLogger := displayLogger.With().Int("pid", pid).Str("scope", "native").Logger()
|
||||
|
||||
onVideoStateChange := opts.OnVideoStateChange
|
||||
if onVideoStateChange == nil {
|
||||
onVideoStateChange = func(state VideoState) {
|
||||
|
|
@ -50,7 +55,7 @@ func NewNative(opts NativeOptions) *Native {
|
|||
onVideoFrameReceived := opts.OnVideoFrameReceived
|
||||
if onVideoFrameReceived == nil {
|
||||
onVideoFrameReceived = func(frame []byte, duration time.Duration) {
|
||||
nativeLogger.Info().Interface("frame", frame).Dur("duration", duration).Msg("video frame received")
|
||||
nativeLogger.Trace().Interface("frame", frame).Dur("duration", duration).Msg("video frame received")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,10 +81,9 @@ func NewNative(opts NativeOptions) *Native {
|
|||
}
|
||||
|
||||
return &Native{
|
||||
disable: opts.Disable,
|
||||
ready: make(chan struct{}),
|
||||
l: nativeLogger,
|
||||
lD: displayLogger,
|
||||
l: &nativeSubLogger,
|
||||
lD: &displaySubLogger,
|
||||
systemVersion: opts.SystemVersion,
|
||||
appVersion: opts.AppVersion,
|
||||
displayRotation: opts.DisplayRotation,
|
||||
|
|
@ -94,13 +98,7 @@ func NewNative(opts NativeOptions) *Native {
|
|||
}
|
||||
}
|
||||
|
||||
func (n *Native) Start() {
|
||||
if n.disable {
|
||||
nativeLogger.Warn().Msg("native is disabled, skipping initialization")
|
||||
setCgoDisabled(true)
|
||||
return
|
||||
}
|
||||
|
||||
func (n *Native) Start() error {
|
||||
// set up singleton
|
||||
setInstance(n)
|
||||
setUpNativeHandlers()
|
||||
|
|
@ -117,9 +115,11 @@ func (n *Native) Start() {
|
|||
|
||||
if err := videoInit(n.defaultQualityFactor); err != nil {
|
||||
n.l.Error().Err(err).Msg("failed to initialize video")
|
||||
return err
|
||||
}
|
||||
|
||||
close(n.ready)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DoNotUseThisIsForCrashTestingOnly
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
# Proto Files
|
||||
|
||||
This directory contains the Protocol Buffer definitions for the native service.
|
||||
|
||||
## Generating Code
|
||||
|
||||
To generate the Go code from the proto files, run:
|
||||
|
||||
```bash
|
||||
./scripts/generate_proto.sh
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
protoc \
|
||||
--go_out=. \
|
||||
--go_opt=paths=source_relative \
|
||||
--go-grpc_out=. \
|
||||
--go-grpc_opt=paths=source_relative \
|
||||
internal/native/proto/native.proto
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `protoc` - Protocol Buffer compiler
|
||||
- `protoc-gen-go` - Go plugin for protoc (install with: `go install google.golang.org/protobuf/cmd/protoc-gen-go@latest`)
|
||||
- `protoc-gen-go-grpc` - gRPC Go plugin for protoc (install with: `go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest`)
|
||||
|
||||
## Note
|
||||
|
||||
The current `native.pb.go` and `native_grpc.pb.go` files are placeholder/stub files. They should be regenerated from `native.proto` using the commands above.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,258 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package native;
|
||||
|
||||
option go_package = "github.com/jetkvm/kvm/internal/native/proto";
|
||||
|
||||
// NativeService provides methods to interact with the native layer
|
||||
service NativeService {
|
||||
// Ready check
|
||||
rpc IsReady(IsReadyRequest) returns (IsReadyResponse);
|
||||
|
||||
// Video methods
|
||||
rpc VideoSetSleepMode(VideoSetSleepModeRequest) returns (Empty);
|
||||
rpc VideoGetSleepMode(Empty) returns (VideoGetSleepModeResponse);
|
||||
rpc VideoSleepModeSupported(Empty) returns (VideoSleepModeSupportedResponse);
|
||||
rpc VideoSetQualityFactor(VideoSetQualityFactorRequest) returns (Empty);
|
||||
rpc VideoGetQualityFactor(Empty) returns (VideoGetQualityFactorResponse);
|
||||
rpc VideoSetEDID(VideoSetEDIDRequest) returns (Empty);
|
||||
rpc VideoGetEDID(Empty) returns (VideoGetEDIDResponse);
|
||||
rpc VideoLogStatus(Empty) returns (VideoLogStatusResponse);
|
||||
rpc VideoStop(Empty) returns (Empty);
|
||||
rpc VideoStart(Empty) returns (Empty);
|
||||
|
||||
// UI methods
|
||||
rpc GetLVGLVersion(Empty) returns (GetLVGLVersionResponse);
|
||||
rpc UIObjHide(UIObjHideRequest) returns (UIObjHideResponse);
|
||||
rpc UIObjShow(UIObjShowRequest) returns (UIObjShowResponse);
|
||||
rpc UISetVar(UISetVarRequest) returns (Empty);
|
||||
rpc UIGetVar(UIGetVarRequest) returns (UIGetVarResponse);
|
||||
rpc UIObjAddState(UIObjAddStateRequest) returns (UIObjAddStateResponse);
|
||||
rpc UIObjClearState(UIObjClearStateRequest) returns (UIObjClearStateResponse);
|
||||
rpc UIObjAddFlag(UIObjAddFlagRequest) returns (UIObjAddFlagResponse);
|
||||
rpc UIObjClearFlag(UIObjClearFlagRequest) returns (UIObjClearFlagResponse);
|
||||
rpc UIObjSetOpacity(UIObjSetOpacityRequest) returns (UIObjSetOpacityResponse);
|
||||
rpc UIObjFadeIn(UIObjFadeInRequest) returns (UIObjFadeInResponse);
|
||||
rpc UIObjFadeOut(UIObjFadeOutRequest) returns (UIObjFadeOutResponse);
|
||||
rpc UIObjSetLabelText(UIObjSetLabelTextRequest) returns (UIObjSetLabelTextResponse);
|
||||
rpc UIObjSetImageSrc(UIObjSetImageSrcRequest) returns (UIObjSetImageSrcResponse);
|
||||
rpc DisplaySetRotation(DisplaySetRotationRequest) returns (DisplaySetRotationResponse);
|
||||
rpc UpdateLabelIfChanged(UpdateLabelIfChangedRequest) returns (Empty);
|
||||
rpc UpdateLabelAndChangeVisibility(UpdateLabelAndChangeVisibilityRequest) returns (Empty);
|
||||
rpc SwitchToScreenIf(SwitchToScreenIfRequest) returns (Empty);
|
||||
rpc SwitchToScreenIfDifferent(SwitchToScreenIfDifferentRequest) returns (Empty);
|
||||
|
||||
// Testing
|
||||
rpc DoNotUseThisIsForCrashTestingOnly(Empty) returns (Empty);
|
||||
|
||||
// Events stream
|
||||
rpc StreamEvents(Empty) returns (stream Event);
|
||||
}
|
||||
|
||||
// Messages
|
||||
message Empty {}
|
||||
|
||||
message IsReadyRequest {}
|
||||
|
||||
message IsReadyResponse {
|
||||
bool ready = 1;
|
||||
string error = 2;
|
||||
bool video_ready = 3;
|
||||
}
|
||||
|
||||
message VideoState {
|
||||
bool ready = 1;
|
||||
string error = 2;
|
||||
int32 width = 3;
|
||||
int32 height = 4;
|
||||
double frame_per_second = 5;
|
||||
}
|
||||
|
||||
message VideoSetSleepModeRequest {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
message VideoGetSleepModeResponse {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
message VideoSleepModeSupportedResponse {
|
||||
bool supported = 1;
|
||||
}
|
||||
|
||||
message VideoSetQualityFactorRequest {
|
||||
double factor = 1;
|
||||
}
|
||||
|
||||
message VideoGetQualityFactorResponse {
|
||||
double factor = 1;
|
||||
}
|
||||
|
||||
message VideoSetEDIDRequest {
|
||||
string edid = 1;
|
||||
}
|
||||
|
||||
message VideoGetEDIDResponse {
|
||||
string edid = 1;
|
||||
}
|
||||
|
||||
message VideoLogStatusResponse {
|
||||
string status = 1;
|
||||
}
|
||||
|
||||
message GetLVGLVersionResponse {
|
||||
string version = 1;
|
||||
}
|
||||
|
||||
message UIObjHideRequest {
|
||||
string obj_name = 1;
|
||||
}
|
||||
|
||||
message UIObjHideResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UIObjShowRequest {
|
||||
string obj_name = 1;
|
||||
}
|
||||
|
||||
message UIObjShowResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UISetVarRequest {
|
||||
string name = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message UIGetVarRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message UIGetVarResponse {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
message UIObjAddStateRequest {
|
||||
string obj_name = 1;
|
||||
string state = 2;
|
||||
}
|
||||
|
||||
message UIObjAddStateResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UIObjClearStateRequest {
|
||||
string obj_name = 1;
|
||||
string state = 2;
|
||||
}
|
||||
|
||||
message UIObjClearStateResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UIObjAddFlagRequest {
|
||||
string obj_name = 1;
|
||||
string flag = 2;
|
||||
}
|
||||
|
||||
message UIObjAddFlagResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UIObjClearFlagRequest {
|
||||
string obj_name = 1;
|
||||
string flag = 2;
|
||||
}
|
||||
|
||||
message UIObjClearFlagResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UIObjSetOpacityRequest {
|
||||
string obj_name = 1;
|
||||
int32 opacity = 2;
|
||||
}
|
||||
|
||||
message UIObjSetOpacityResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UIObjFadeInRequest {
|
||||
string obj_name = 1;
|
||||
uint32 duration = 2;
|
||||
}
|
||||
|
||||
message UIObjFadeInResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UIObjFadeOutRequest {
|
||||
string obj_name = 1;
|
||||
uint32 duration = 2;
|
||||
}
|
||||
|
||||
message UIObjFadeOutResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UIObjSetLabelTextRequest {
|
||||
string obj_name = 1;
|
||||
string text = 2;
|
||||
}
|
||||
|
||||
message UIObjSetLabelTextResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UIObjSetImageSrcRequest {
|
||||
string obj_name = 1;
|
||||
string image = 2;
|
||||
}
|
||||
|
||||
message UIObjSetImageSrcResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message DisplaySetRotationRequest {
|
||||
uint32 rotation = 1;
|
||||
}
|
||||
|
||||
message DisplaySetRotationResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message UpdateLabelIfChangedRequest {
|
||||
string obj_name = 1;
|
||||
string new_text = 2;
|
||||
}
|
||||
|
||||
message UpdateLabelAndChangeVisibilityRequest {
|
||||
string obj_name = 1;
|
||||
string new_text = 2;
|
||||
}
|
||||
|
||||
message SwitchToScreenIfRequest {
|
||||
string screen_name = 1;
|
||||
repeated string should_switch = 2;
|
||||
}
|
||||
|
||||
message SwitchToScreenIfDifferentRequest {
|
||||
string screen_name = 1;
|
||||
}
|
||||
|
||||
message Event {
|
||||
string type = 1;
|
||||
oneof data {
|
||||
VideoState video_state = 2;
|
||||
string indev_event = 3;
|
||||
string rpc_event = 4;
|
||||
VideoFrame video_frame = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message VideoFrame {
|
||||
bytes frame = 1;
|
||||
int64 duration_ns = 2;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,687 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/jetkvm/kvm/internal/utils"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
maxFrameSize = 1920 * 1080 / 2
|
||||
defaultMaxRestartAttempts uint = 5
|
||||
)
|
||||
|
||||
type nativeProxyOptions struct {
|
||||
Disable bool `env:"JETKVM_NATIVE_DISABLE"`
|
||||
SystemVersion *semver.Version `env:"JETKVM_NATIVE_SYSTEM_VERSION"`
|
||||
AppVersion *semver.Version `env:"JETKVM_NATIVE_APP_VERSION"`
|
||||
DisplayRotation uint16 `env:"JETKVM_NATIVE_DISPLAY_ROTATION"`
|
||||
DefaultQualityFactor float64 `env:"JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR"`
|
||||
CtrlUnixSocket string `env:"JETKVM_NATIVE_CTRL_UNIX_SOCKET"`
|
||||
VideoStreamUnixSocket string `env:"JETKVM_NATIVE_VIDEO_STREAM_UNIX_SOCKET"`
|
||||
BinaryPath string `env:"JETKVM_NATIVE_BINARY_PATH"`
|
||||
LoggerLevel zerolog.Level `env:"JETKVM_NATIVE_LOGGER_LEVEL"`
|
||||
HandshakeMessage string `env:"JETKVM_NATIVE_HANDSHAKE_MESSAGE"`
|
||||
MaxRestartAttempts uint
|
||||
|
||||
OnVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||
OnIndevEvent func(event string)
|
||||
OnRpcEvent func(event string)
|
||||
OnVideoStateChange func(state VideoState)
|
||||
OnNativeRestart func()
|
||||
}
|
||||
|
||||
func randomId(binaryLength int) string {
|
||||
s := make([]byte, binaryLength)
|
||||
_, err := rand.Read(s)
|
||||
if err != nil {
|
||||
nativeLogger.Error().Err(err).Msg("failed to generate random ID")
|
||||
return strings.Repeat("0", binaryLength*2) // return all zeros if error
|
||||
}
|
||||
return hex.EncodeToString(s)
|
||||
}
|
||||
|
||||
func (n *NativeOptions) toProxyOptions() *nativeProxyOptions {
|
||||
// random 16 bytes hex string
|
||||
handshakeMessage := randomId(16)
|
||||
maxRestartAttempts := defaultMaxRestartAttempts
|
||||
if n.MaxRestartAttempts > 0 {
|
||||
maxRestartAttempts = n.MaxRestartAttempts
|
||||
}
|
||||
return &nativeProxyOptions{
|
||||
SystemVersion: n.SystemVersion,
|
||||
AppVersion: n.AppVersion,
|
||||
DisplayRotation: n.DisplayRotation,
|
||||
DefaultQualityFactor: n.DefaultQualityFactor,
|
||||
OnVideoFrameReceived: n.OnVideoFrameReceived,
|
||||
OnIndevEvent: n.OnIndevEvent,
|
||||
OnRpcEvent: n.OnRpcEvent,
|
||||
OnVideoStateChange: n.OnVideoStateChange,
|
||||
OnNativeRestart: n.OnNativeRestart,
|
||||
HandshakeMessage: handshakeMessage,
|
||||
MaxRestartAttempts: maxRestartAttempts,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *nativeProxyOptions) toNativeOptions() *NativeOptions {
|
||||
return &NativeOptions{
|
||||
SystemVersion: p.SystemVersion,
|
||||
AppVersion: p.AppVersion,
|
||||
DisplayRotation: p.DisplayRotation,
|
||||
DefaultQualityFactor: p.DefaultQualityFactor,
|
||||
}
|
||||
}
|
||||
|
||||
// cmdWrapper wraps exec.Cmd to implement processCmd interface
|
||||
type cmdWrapper struct {
|
||||
*exec.Cmd
|
||||
stdoutHandler *nativeProxyStdoutHandler
|
||||
}
|
||||
|
||||
func (c *cmdWrapper) GetProcess() interface {
|
||||
Kill() error
|
||||
Signal(sig interface{}) error
|
||||
} {
|
||||
return &processWrapper{Process: c.Process}
|
||||
}
|
||||
|
||||
type processWrapper struct {
|
||||
*os.Process
|
||||
}
|
||||
|
||||
func (p *processWrapper) Signal(sig interface{}) error {
|
||||
if sig == nil {
|
||||
// Check if process is alive by sending signal 0
|
||||
return p.Process.Signal(os.Signal(syscall.Signal(0)))
|
||||
}
|
||||
if s, ok := sig.(os.Signal); ok {
|
||||
return p.Process.Signal(s)
|
||||
}
|
||||
return fmt.Errorf("invalid signal type")
|
||||
}
|
||||
|
||||
// NativeProxy is a proxy that communicates with a separate native process
|
||||
type NativeProxy struct {
|
||||
nativeUnixSocket string
|
||||
videoStreamUnixSocket string
|
||||
videoStreamListener net.Listener
|
||||
binaryPath string
|
||||
|
||||
startMu sync.Mutex // mutex for the start process (context and isStopped)
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
client *GRPCClient
|
||||
clientMu sync.RWMutex // mutex for the client
|
||||
|
||||
cmd *cmdWrapper
|
||||
cmdMu sync.Mutex // mutex for the cmd
|
||||
|
||||
logger *zerolog.Logger
|
||||
options *nativeProxyOptions
|
||||
restarts uint
|
||||
stopped bool
|
||||
}
|
||||
|
||||
// NewNativeProxy creates a new NativeProxy that spawns a separate process
|
||||
func NewNativeProxy(opts NativeOptions) (*NativeProxy, error) {
|
||||
proxyOptions := opts.toProxyOptions()
|
||||
proxyOptions.VideoStreamUnixSocket = fmt.Sprintf("@jetkvm/native/video-stream/%s", randomId(4))
|
||||
|
||||
// Get the current executable path to spawn itself
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
proxy := &NativeProxy{
|
||||
nativeUnixSocket: proxyOptions.CtrlUnixSocket,
|
||||
videoStreamUnixSocket: proxyOptions.VideoStreamUnixSocket,
|
||||
binaryPath: exePath,
|
||||
logger: nativeLogger,
|
||||
options: proxyOptions,
|
||||
restarts: 0,
|
||||
}
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func (p *NativeProxy) startVideoStreamListener() error {
|
||||
if p.videoStreamListener != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger := p.logger.With().Str("socketPath", p.videoStreamUnixSocket).Logger()
|
||||
listener, err := net.Listen("unixpacket", p.videoStreamUnixSocket)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to start video stream listener")
|
||||
return fmt.Errorf("failed to start video stream listener: %w", err)
|
||||
}
|
||||
logger.Info().Msg("video stream listener started")
|
||||
p.videoStreamListener = listener
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to accept socket")
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info().Msg("video stream socket accepted")
|
||||
go p.handleVideoFrame(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type nativeProxyStdoutHandler struct {
|
||||
mu *sync.Mutex
|
||||
handshakeCh chan bool
|
||||
handshakeMessage string
|
||||
handshakeDone bool
|
||||
}
|
||||
|
||||
func (w *nativeProxyStdoutHandler) Write(p []byte) (n int, err error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if !w.handshakeDone && strings.Contains(string(p), w.handshakeMessage) {
|
||||
w.handshakeDone = true
|
||||
w.handshakeCh <- true
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
os.Stdout.Write(p)
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (p *NativeProxy) toProcessCommand() (*cmdWrapper, error) {
|
||||
// generate a new random ID for the gRPC socket on each restart
|
||||
// sometimes the socket is not closed properly when the process exits
|
||||
// this is a workaround to avoid the issue
|
||||
p.nativeUnixSocket = fmt.Sprintf("jetkvm/native/grpc/%s", randomId(4))
|
||||
p.options.CtrlUnixSocket = p.nativeUnixSocket
|
||||
|
||||
envArgs, err := utils.MarshalEnv(p.options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal environment variables: %w", err)
|
||||
}
|
||||
|
||||
cmd := &cmdWrapper{
|
||||
Cmd: exec.Command(
|
||||
p.binaryPath,
|
||||
"-subcomponent=native",
|
||||
),
|
||||
stdoutHandler: &nativeProxyStdoutHandler{
|
||||
mu: &sync.Mutex{},
|
||||
handshakeCh: make(chan bool),
|
||||
handshakeMessage: p.options.HandshakeMessage,
|
||||
},
|
||||
}
|
||||
cmd.Stdout = cmd.stdoutHandler
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
Pdeathsig: syscall.SIGTERM,
|
||||
}
|
||||
// Set environment variable to indicate native process mode
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
envArgs...,
|
||||
)
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func (p *NativeProxy) handleVideoFrame(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
inboundPacket := make([]byte, maxFrameSize)
|
||||
lastFrame := time.Now()
|
||||
|
||||
for {
|
||||
n, err := conn.Read(inboundPacket)
|
||||
if err != nil {
|
||||
p.logger.Warn().Err(err).Msg("failed to read video frame from socket")
|
||||
break
|
||||
}
|
||||
now := time.Now()
|
||||
sinceLastFrame := now.Sub(lastFrame)
|
||||
lastFrame = now
|
||||
p.options.OnVideoFrameReceived(inboundPacket[:n], sinceLastFrame)
|
||||
}
|
||||
}
|
||||
|
||||
// it should be only called by start() method, as it isn't thread-safe
|
||||
func (p *NativeProxy) setUpGRPCClient() error {
|
||||
// wait until handshake completed
|
||||
select {
|
||||
case <-p.cmd.stdoutHandler.handshakeCh:
|
||||
p.logger.Info().Msg("handshake completed")
|
||||
case <-time.After(10 * time.Second):
|
||||
return fmt.Errorf("handshake not completed within 10 seconds")
|
||||
}
|
||||
|
||||
logger := p.logger.With().Str("socketPath", "@"+p.nativeUnixSocket).Logger()
|
||||
client, err := NewGRPCClient(grpcClientOptions{
|
||||
SocketPath: p.nativeUnixSocket,
|
||||
Logger: &logger,
|
||||
OnIndevEvent: p.options.OnIndevEvent,
|
||||
OnRpcEvent: p.options.OnRpcEvent,
|
||||
OnVideoStateChange: p.options.OnVideoStateChange,
|
||||
})
|
||||
|
||||
logger.Info().Msg("created gRPC client")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create gRPC client: %w", err)
|
||||
}
|
||||
p.client = client
|
||||
|
||||
// Wait for ready signal from the native process
|
||||
if err := p.client.WaitReady(); err != nil {
|
||||
// Clean up if ready failed
|
||||
if p.cmd.Process != nil {
|
||||
_ = p.cmd.Process.Kill()
|
||||
_ = p.cmd.Wait()
|
||||
}
|
||||
return fmt.Errorf("failed to wait for ready: %w", err)
|
||||
}
|
||||
|
||||
// Call on native restart callback if it exists and restarts are greater than 0
|
||||
if p.options.OnNativeRestart != nil && p.restarts > 0 {
|
||||
go p.options.OnNativeRestart()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *NativeProxy) doStart() error {
|
||||
p.cmdMu.Lock()
|
||||
defer p.cmdMu.Unlock()
|
||||
|
||||
// lock OS thread to prevent the process from being moved to a different thread
|
||||
// see also https://go.dev/issue/27505
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
cmd, err := p.toProcessCommand()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create process: %w", err)
|
||||
}
|
||||
|
||||
p.cmd = cmd
|
||||
|
||||
if err := p.cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start native process: %w", err)
|
||||
}
|
||||
|
||||
// here we'll replace the logger with a new one that includes the process ID
|
||||
// there's no need to lock the mutex here as the side effect is acceptable
|
||||
newLogger := p.logger.With().Int("pid", p.cmd.Process.Pid).Logger()
|
||||
p.logger = &newLogger
|
||||
|
||||
p.logger.Info().Msg("native process started")
|
||||
|
||||
if err := p.setUpGRPCClient(); err != nil {
|
||||
return fmt.Errorf("failed to set up gRPC client: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the native process
|
||||
func (p *NativeProxy) Start() error {
|
||||
p.startMu.Lock()
|
||||
defer p.startMu.Unlock()
|
||||
|
||||
p.ctx, p.cancel = context.WithCancel(context.Background())
|
||||
|
||||
if p.stopped {
|
||||
return fmt.Errorf("proxy is stopped")
|
||||
}
|
||||
|
||||
if err := p.startVideoStreamListener(); err != nil {
|
||||
return fmt.Errorf("failed to start video stream listener: %w", err)
|
||||
}
|
||||
|
||||
if err := p.doStart(); err != nil {
|
||||
return fmt.Errorf("failed to start native process: %w", err)
|
||||
}
|
||||
|
||||
go p.monitorProcess()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// monitorProcess monitors the native process and restarts it if it crashes
|
||||
func (p *NativeProxy) monitorProcess() {
|
||||
for {
|
||||
if p.stopped {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
p.logger.Trace().Msg("context done, stopping monitor process [before wait]")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
p.cmdMu.Lock()
|
||||
err := fmt.Errorf("native process not started")
|
||||
if p.cmd != nil {
|
||||
err = p.cmd.Wait()
|
||||
}
|
||||
p.cmdMu.Unlock()
|
||||
|
||||
if p.stopped {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
p.logger.Trace().Msg("context done, stopping monitor process [after wait]")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
p.logger.Warn().Err(err).Msg("native process exited, restarting ...")
|
||||
|
||||
// Wait a bit before restarting
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Restart the process
|
||||
if err := p.restartProcess(); err != nil {
|
||||
p.logger.Error().Err(err).Msg("failed to restart native process")
|
||||
// Wait longer before retrying
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// restartProcess restarts the native process
|
||||
func (p *NativeProxy) restartProcess() error {
|
||||
p.restarts++
|
||||
logger := p.logger.With().Uint("attempt", p.restarts).Uint("maxAttempts", p.options.MaxRestartAttempts).Logger()
|
||||
|
||||
if p.restarts >= p.options.MaxRestartAttempts {
|
||||
logger.Fatal().Msg("max restart attempts reached, exiting")
|
||||
return fmt.Errorf("max restart attempts reached")
|
||||
}
|
||||
|
||||
if p.stopped {
|
||||
return fmt.Errorf("proxy is stopped")
|
||||
}
|
||||
|
||||
// Close old client
|
||||
p.clientMu.Lock()
|
||||
if p.client != nil {
|
||||
if err := p.client.Close(); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to close gRPC client")
|
||||
}
|
||||
p.client = nil // set to nil to avoid closing it again
|
||||
}
|
||||
p.clientMu.Unlock()
|
||||
logger.Info().Msg("gRPC client closed")
|
||||
|
||||
logger.Info().Msg("attempting to restart native process")
|
||||
if err := p.doStart(); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to start native process")
|
||||
return fmt.Errorf("failed to start native process: %w", err)
|
||||
}
|
||||
|
||||
logger.Info().Msg("native process restarted successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the native process
|
||||
func (p *NativeProxy) Stop() error {
|
||||
p.startMu.Lock()
|
||||
defer p.startMu.Unlock()
|
||||
|
||||
p.stopped = true
|
||||
|
||||
if p.cmd.Process != nil {
|
||||
if err := p.cmd.Process.Kill(); err != nil {
|
||||
return fmt.Errorf("failed to kill native process: %w", err)
|
||||
}
|
||||
_ = p.cmd.Wait()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func zeroValue[V string | bool | float64]() V {
|
||||
var v V
|
||||
return v
|
||||
}
|
||||
|
||||
func nativeProxyClientExec[K comparable, V string | bool | float64](p *NativeProxy, fn func(*GRPCClient) (V, error)) (V, error) {
|
||||
p.clientMu.RLock()
|
||||
defer p.clientMu.RUnlock()
|
||||
|
||||
if p.client == nil {
|
||||
return zeroValue[V](), fmt.Errorf("gRPC client not initialized")
|
||||
}
|
||||
|
||||
return fn(p.client)
|
||||
}
|
||||
|
||||
func nativeProxyClientExecWithoutArgument(p *NativeProxy, fn func(*GRPCClient) error) error {
|
||||
p.clientMu.RLock()
|
||||
defer p.clientMu.RUnlock()
|
||||
|
||||
if p.client == nil {
|
||||
return fmt.Errorf("gRPC client not initialized")
|
||||
}
|
||||
|
||||
return fn(p.client)
|
||||
}
|
||||
|
||||
// Implement all Native methods by forwarding to gRPC client
|
||||
func (p *NativeProxy) VideoSetSleepMode(enabled bool) error {
|
||||
return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error {
|
||||
return client.VideoSetSleepMode(enabled)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) VideoGetSleepMode() (bool, error) {
|
||||
return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.VideoGetSleepMode()
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) VideoSleepModeSupported() bool {
|
||||
result, _ := nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.VideoSleepModeSupported(), nil
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *NativeProxy) VideoSetQualityFactor(factor float64) error {
|
||||
return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error {
|
||||
return client.VideoSetQualityFactor(factor)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) VideoGetQualityFactor() (float64, error) {
|
||||
return nativeProxyClientExec[float64](p, func(client *GRPCClient) (float64, error) {
|
||||
return client.VideoGetQualityFactor()
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) VideoSetEDID(edid string) error {
|
||||
return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error {
|
||||
return client.VideoSetEDID(edid)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) VideoGetEDID() (string, error) {
|
||||
return nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) {
|
||||
return client.VideoGetEDID()
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) VideoLogStatus() (string, error) {
|
||||
return nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) {
|
||||
return client.VideoLogStatus()
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) VideoStop() error {
|
||||
return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error {
|
||||
return client.VideoStop()
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) VideoStart() error {
|
||||
return nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error {
|
||||
return client.VideoStart()
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) GetLVGLVersion() (string, error) {
|
||||
return nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) {
|
||||
return client.GetLVGLVersion()
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIObjHide(objName string) (bool, error) {
|
||||
result, err := nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.UIObjHide(objName)
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIObjShow(objName string) (bool, error) {
|
||||
result, err := nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.UIObjShow(objName)
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UISetVar(name string, value string) {
|
||||
_ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error {
|
||||
client.UISetVar(name, value)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIGetVar(name string) string {
|
||||
result, _ := nativeProxyClientExec[string](p, func(client *GRPCClient) (string, error) {
|
||||
return client.UIGetVar(name), nil
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIObjAddState(objName string, state string) (bool, error) {
|
||||
return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.UIObjAddState(objName, state)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIObjClearState(objName string, state string) (bool, error) {
|
||||
return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.UIObjClearState(objName, state)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIObjAddFlag(objName string, flag string) (bool, error) {
|
||||
return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.UIObjAddFlag(objName, flag)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIObjClearFlag(objName string, flag string) (bool, error) {
|
||||
return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.UIObjClearFlag(objName, flag)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIObjFadeIn(objName string, duration uint32) (bool, error) {
|
||||
return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.UIObjFadeIn(objName, duration)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIObjFadeOut(objName string, duration uint32) (bool, error) {
|
||||
return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.UIObjFadeOut(objName, duration)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIObjSetLabelText(objName string, text string) (bool, error) {
|
||||
return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.UIObjSetLabelText(objName, text)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIObjSetImageSrc(objName string, image string) (bool, error) {
|
||||
return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.UIObjSetImageSrc(objName, image)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UIObjSetOpacity(objName string, opacity int) (bool, error) {
|
||||
return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.UIObjSetOpacity(objName, opacity)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) DisplaySetRotation(rotation uint16) (bool, error) {
|
||||
return nativeProxyClientExec[bool](p, func(client *GRPCClient) (bool, error) {
|
||||
return client.DisplaySetRotation(rotation)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UpdateLabelIfChanged(objName string, newText string) {
|
||||
_ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error {
|
||||
client.UpdateLabelIfChanged(objName, newText)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) UpdateLabelAndChangeVisibility(objName string, newText string) {
|
||||
_ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error {
|
||||
client.UpdateLabelAndChangeVisibility(objName, newText)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) SwitchToScreenIf(screenName string, shouldSwitch []string) {
|
||||
_ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error {
|
||||
client.SwitchToScreenIf(screenName, shouldSwitch)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) SwitchToScreenIfDifferent(screenName string) {
|
||||
_ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error {
|
||||
client.SwitchToScreenIfDifferent(screenName)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *NativeProxy) DoNotUseThisIsForCrashTestingOnly() {
|
||||
_ = nativeProxyClientExecWithoutArgument(p, func(client *GRPCClient) error {
|
||||
client.DoNotUseThisIsForCrashTestingOnly()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
"github.com/erikdubbelboer/gspt"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Native Process
|
||||
// stdout - exchange messages with the parent process
|
||||
// stderr - logging and error messages
|
||||
|
||||
var (
|
||||
procPrefix string = "jetkvm: [native]"
|
||||
lastProcTitle string
|
||||
)
|
||||
|
||||
const (
|
||||
DebugModeFile = "/userdata/jetkvm/.native-debug-mode"
|
||||
)
|
||||
|
||||
func setProcTitle(status string) {
|
||||
lastProcTitle = status
|
||||
if status != "" {
|
||||
status = " " + status
|
||||
}
|
||||
title := fmt.Sprintf("%s%s", procPrefix, status)
|
||||
gspt.SetProcTitle(title)
|
||||
}
|
||||
|
||||
func monitorCrashSignal(ctx context.Context, logger *zerolog.Logger, nativeInstance NativeInterface) {
|
||||
logger.Info().Msg("DEBUG mode: will crash the process on SIGHUP signal")
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGHUP)
|
||||
|
||||
for {
|
||||
select {
|
||||
case sig := <-sigChan:
|
||||
logger.Info().Str("signal", sig.String()).Msg("received termination signal")
|
||||
nativeInstance.DoNotUseThisIsForCrashTestingOnly()
|
||||
case <-ctx.Done():
|
||||
logger.Info().Msg("context done, stopping monitor process")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunNativeProcess runs the native process mode
|
||||
func RunNativeProcess(binaryName string) {
|
||||
appCtx, appCtxCancel := context.WithCancel(context.Background())
|
||||
defer appCtxCancel()
|
||||
|
||||
logger := nativeLogger.With().Int("pid", os.Getpid()).Logger()
|
||||
setProcTitle("starting")
|
||||
|
||||
// Parse native options
|
||||
var proxyOptions nativeProxyOptions
|
||||
if err := env.Parse(&proxyOptions); err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to parse native proxy options")
|
||||
}
|
||||
|
||||
// Connect to video stream socket
|
||||
conn, err := net.Dial("unixpacket", proxyOptions.VideoStreamUnixSocket)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to connect to video stream socket")
|
||||
}
|
||||
logger.Info().Str("videoStreamSocketPath", proxyOptions.VideoStreamUnixSocket).Msg("connected to video stream socket")
|
||||
|
||||
nativeOptions := proxyOptions.toNativeOptions()
|
||||
nativeOptions.OnVideoFrameReceived = func(frame []byte, duration time.Duration) {
|
||||
_, err := conn.Write(frame)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to write frame to video stream socket")
|
||||
}
|
||||
}
|
||||
|
||||
// Create native instance
|
||||
nativeInstance := NewNative(*nativeOptions)
|
||||
gspt.SetProcTitle("jetkvm: [native] initializing")
|
||||
|
||||
// Start native instance
|
||||
if err := nativeInstance.Start(); err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to start native instance")
|
||||
}
|
||||
|
||||
grpcLogger := logger.With().Str("socketPath", fmt.Sprintf("@%v", proxyOptions.CtrlUnixSocket)).Logger()
|
||||
setProcTitle("starting gRPC server")
|
||||
// Create gRPC server
|
||||
grpcServer := NewGRPCServer(nativeInstance, &grpcLogger)
|
||||
|
||||
logger.Info().Msg("starting gRPC server")
|
||||
// Start gRPC server
|
||||
server, lis, err := StartGRPCServer(grpcServer, fmt.Sprintf("@%v", proxyOptions.CtrlUnixSocket), &logger)
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to start gRPC server")
|
||||
}
|
||||
setProcTitle("ready")
|
||||
|
||||
if _, err := os.Stat(DebugModeFile); err == nil {
|
||||
logger.Info().Msg("DEBUG mode: enabled")
|
||||
go monitorCrashSignal(appCtx, &logger, nativeInstance)
|
||||
}
|
||||
|
||||
// Signal that we're ready by writing handshake message to stdout (for parent to read)
|
||||
// Stdout.Write is used to avoid buffering the message
|
||||
_, err = os.Stdout.Write([]byte(proxyOptions.HandshakeMessage + "\n"))
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("failed to write handshake message to stdout")
|
||||
}
|
||||
|
||||
// Set up signal handling
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
|
||||
|
||||
// Wait for signal
|
||||
sig := <-sigChan
|
||||
logger.Info().
|
||||
Str("signal", sig.String()).
|
||||
Msg("received termination signal")
|
||||
|
||||
// Graceful shutdown might stuck forever,
|
||||
// we will use Stop() instead to force quit the gRPC server,
|
||||
// we can implement a graceful shutdown with a timeout in the future if needed
|
||||
server.Stop()
|
||||
lis.Close()
|
||||
|
||||
logger.Info().Msg("native process exiting")
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package supervisor
|
||||
|
||||
const (
|
||||
EnvChildID = "JETKVM_CHILD_ID" // The child ID is the version of the app that is running
|
||||
EnvSubcomponent = "JETKVM_SUBCOMPONENT" // The subcomponent is the component that is running
|
||||
ErrorDumpDir = "/userdata/jetkvm/crashdump" // The error dump directory is the directory where the error dumps are stored
|
||||
ErrorDumpLastFile = "last-crash.log" // The error dump last file is the last error dump file
|
||||
ErrorDumpTemplate = "jetkvm-%s.log" // The error dump template is the template for the error dump file
|
||||
)
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func MarshalEnv(instance interface{}) ([]string, error) {
|
||||
v := reflect.ValueOf(instance)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return nil, fmt.Errorf("instance is nil")
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("instance must be a struct or pointer to struct")
|
||||
}
|
||||
|
||||
t := v.Type()
|
||||
var result []string
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
fieldValue := v.Field(i)
|
||||
|
||||
// Get the env tag
|
||||
envTag := field.Tag.Get("env")
|
||||
if envTag == "" || envTag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip unexported fields
|
||||
if !fieldValue.CanInterface() {
|
||||
continue
|
||||
}
|
||||
|
||||
var valueStr string
|
||||
|
||||
// Handle different types
|
||||
switch fieldValue.Kind() {
|
||||
case reflect.Bool:
|
||||
valueStr = strconv.FormatBool(fieldValue.Bool())
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
valueStr = strconv.FormatUint(fieldValue.Uint(), 10)
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
valueStr = strconv.FormatInt(fieldValue.Int(), 10)
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
valueStr = strconv.FormatFloat(fieldValue.Float(), 'f', -1, 64)
|
||||
|
||||
case reflect.String:
|
||||
valueStr = fieldValue.String()
|
||||
|
||||
case reflect.Ptr:
|
||||
if fieldValue.IsNil() {
|
||||
continue // Skip nil pointers
|
||||
}
|
||||
elem := fieldValue.Elem()
|
||||
// Handle *semver.Version and other pointer types
|
||||
if elem.CanInterface() {
|
||||
if stringer, ok := elem.Interface().(fmt.Stringer); ok {
|
||||
valueStr = stringer.String()
|
||||
} else {
|
||||
valueStr = fmt.Sprintf("%v", elem.Interface())
|
||||
}
|
||||
} else {
|
||||
valueStr = fmt.Sprintf("%v", elem.Interface())
|
||||
}
|
||||
|
||||
default:
|
||||
// For other types, try to convert to string
|
||||
if fieldValue.CanInterface() {
|
||||
if stringer, ok := fieldValue.Interface().(fmt.Stringer); ok {
|
||||
valueStr = stringer.String()
|
||||
} else {
|
||||
valueStr = fmt.Sprintf("%v", fieldValue.Interface())
|
||||
}
|
||||
} else {
|
||||
valueStr = fmt.Sprintf("%v", fieldValue.Interface())
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, fmt.Sprintf("%s=%s", envTag, valueStr))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
type nativeOptions struct {
|
||||
Disable bool `env:"JETKVM_NATIVE_DISABLE"`
|
||||
SystemVersion *semver.Version `env:"JETKVM_NATIVE_SYSTEM_VERSION"`
|
||||
AppVersion *semver.Version `env:"JETKVM_NATIVE_APP_VERSION"`
|
||||
DisplayRotation uint16 `env:"JETKVM_NATIVE_DISPLAY_ROTATION"`
|
||||
DefaultQualityFactor float64 `env:"JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR"`
|
||||
}
|
||||
|
||||
func TestMarshalEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
instance interface{}
|
||||
want []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic struct",
|
||||
instance: nativeOptions{
|
||||
Disable: false,
|
||||
SystemVersion: semver.MustParse("1.1.0"),
|
||||
AppVersion: semver.MustParse("1111.0.0"),
|
||||
DisplayRotation: 1,
|
||||
DefaultQualityFactor: 1.0,
|
||||
},
|
||||
want: []string{
|
||||
"JETKVM_NATIVE_DISABLE=false",
|
||||
"JETKVM_NATIVE_SYSTEM_VERSION=1.1.0",
|
||||
"JETKVM_NATIVE_APP_VERSION=1111.0.0",
|
||||
"JETKVM_NATIVE_DISPLAY_ROTATION=1",
|
||||
"JETKVM_NATIVE_DEFAULT_QUALITY_FACTOR=1",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := MarshalEnv(tt.instance)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("MarshalEnv() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("MarshalEnv() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
25
main.go
25
main.go
|
|
@ -2,21 +2,37 @@ package kvm
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/erikdubbelboer/gspt"
|
||||
"github.com/gwatts/rootcerts"
|
||||
"github.com/jetkvm/kvm/internal/ota"
|
||||
)
|
||||
|
||||
var appCtx context.Context
|
||||
var procPrefix string = "jetkvm: [app]"
|
||||
|
||||
func setProcTitle(status string) {
|
||||
if status != "" {
|
||||
status = " " + status
|
||||
}
|
||||
title := fmt.Sprintf("%s%s", procPrefix, status)
|
||||
gspt.SetProcTitle(title)
|
||||
}
|
||||
|
||||
func Main() {
|
||||
setProcTitle("starting")
|
||||
|
||||
logger.Log().Msg("JetKVM Starting Up")
|
||||
|
||||
checkFailsafeReason()
|
||||
if failsafeModeActive {
|
||||
procPrefix = "jetkvm: [app+failsafe]"
|
||||
logger.Warn().Str("reason", failsafeModeReason).Msg("failsafe mode activated")
|
||||
}
|
||||
|
||||
|
|
@ -38,6 +54,7 @@ func Main() {
|
|||
|
||||
go runWatchdog()
|
||||
|
||||
setProcTitle("initNative")
|
||||
initNative(systemVersionLocal, appVersionLocal)
|
||||
initDisplay()
|
||||
|
||||
|
|
@ -59,6 +76,7 @@ func Main() {
|
|||
http.DefaultClient.Timeout = 1 * time.Minute
|
||||
|
||||
// Initialize network
|
||||
setProcTitle("initNetwork")
|
||||
if err := initNetwork(); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to initialize network")
|
||||
// TODO: reset config to default
|
||||
|
|
@ -66,17 +84,21 @@ func Main() {
|
|||
}
|
||||
|
||||
// Initialize time sync
|
||||
setProcTitle("initTimeSync")
|
||||
initTimeSync()
|
||||
timeSync.Start()
|
||||
|
||||
// Initialize mDNS
|
||||
setProcTitle("initMdns")
|
||||
if err := initMdns(); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to initialize mDNS")
|
||||
}
|
||||
|
||||
setProcTitle("initPrometheus")
|
||||
initPrometheus()
|
||||
|
||||
// initialize usb gadget
|
||||
setProcTitle("initUsbGadget")
|
||||
initUsbGadget()
|
||||
if err := setInitialVirtualMediaState(); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to set initial virtual media state")
|
||||
|
|
@ -138,6 +160,9 @@ func Main() {
|
|||
go RunWebsocketClient()
|
||||
|
||||
initSerialPort()
|
||||
|
||||
setProcTitle("ready")
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigs
|
||||
|
|
|
|||
24
native.go
24
native.go
|
|
@ -11,17 +11,28 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
nativeInstance *native.Native
|
||||
nativeInstance native.NativeInterface
|
||||
nativeCmdLock = sync.Mutex{}
|
||||
)
|
||||
|
||||
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
||||
nativeInstance = native.NewNative(native.NativeOptions{
|
||||
Disable: failsafeModeActive,
|
||||
if failsafeModeActive {
|
||||
nativeInstance = &native.EmptyNativeInterface{}
|
||||
nativeLogger.Warn().Msg("failsafe mode active, using empty native interface")
|
||||
return
|
||||
}
|
||||
|
||||
nativeLogger.Info().Msg("initializing native proxy")
|
||||
var err error
|
||||
nativeInstance, err = native.NewNativeProxy(native.NativeOptions{
|
||||
SystemVersion: systemVersion,
|
||||
AppVersion: appVersion,
|
||||
DisplayRotation: config.GetDisplayRotation(),
|
||||
DefaultQualityFactor: config.VideoQualityFactor,
|
||||
MaxRestartAttempts: config.NativeMaxRestart,
|
||||
OnNativeRestart: func() {
|
||||
configureDisplayOnNativeRestart()
|
||||
},
|
||||
OnVideoStateChange: func(state native.VideoState) {
|
||||
lastVideoState = state
|
||||
triggerVideoStateUpdate()
|
||||
|
|
@ -58,8 +69,13 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
|||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
nativeLogger.Fatal().Err(err).Msg("failed to create native proxy")
|
||||
}
|
||||
|
||||
nativeInstance.Start()
|
||||
if err := nativeInstance.Start(); err != nil {
|
||||
nativeLogger.Fatal().Err(err).Msg("failed to start native proxy")
|
||||
}
|
||||
go func() {
|
||||
if err := nativeInstance.VideoSetEDID(config.EdidString); err != nil {
|
||||
nativeLogger.Warn().Err(err).Msg("error setting EDID")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ set -e
|
|||
SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))")
|
||||
source ${SCRIPT_PATH}/build_utils.sh
|
||||
|
||||
CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-Release}
|
||||
|
||||
CGO_PATH=$(realpath "${SCRIPT_PATH}/../internal/native/cgo")
|
||||
BUILD_DIR=${CGO_PATH}/build
|
||||
|
||||
|
|
@ -31,7 +33,7 @@ VERBOSE=1 cmake -B "${BUILD_DIR}" \
|
|||
-DCONFIG_LV_BUILD_EXAMPLES=OFF \
|
||||
-DCONFIG_LV_BUILD_DEMOS=OFF \
|
||||
-DSKIP_GLIBC_NAMES=ON \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \
|
||||
-DCMAKE_INSTALL_PREFIX="${TMP_DIR}"
|
||||
|
||||
msg_info "▶ Copying built library and header files"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
|
||||
DEFAULT_C_INTELLISENSE_SETTINGS = {
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Linux",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**"
|
||||
],
|
||||
"defines": [],
|
||||
# "compilerPath": "/opt/jetkvm-native-buildkit/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc",
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "gnu++17",
|
||||
"intelliSenseMode": "linux-gcc-arm",
|
||||
"configurationProvider": "ms-vscode.cmake-tools"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
|
||||
def configure_c_intellisense():
|
||||
settings_path = os.path.join('.vscode', 'c_cpp_properties.json')
|
||||
settings = DEFAULT_C_INTELLISENSE_SETTINGS.copy()
|
||||
|
||||
# open existing settings if they exist
|
||||
if os.path.exists(settings_path):
|
||||
with open(settings_path, 'r') as f:
|
||||
settings = json.load(f)
|
||||
|
||||
# update compiler path
|
||||
settings['configurations'][0]['compilerPath'] = "/opt/jetkvm-native-buildkit/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc"
|
||||
settings['configurations'][0]['configurationProvider'] = "ms-vscode.cmake-tools"
|
||||
|
||||
with open(settings_path, 'w') as f:
|
||||
json.dump(settings, f, indent=4)
|
||||
|
||||
print("C/C++ IntelliSense configuration updated.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
configure_c_intellisense()
|
||||
|
|
@ -12,11 +12,14 @@ show_help() {
|
|||
echo
|
||||
echo "Optional:"
|
||||
echo " -u, --user <remote_user> Remote username (default: root)"
|
||||
echo " --gdb-port <port> GDB debug port (default: 2345)"
|
||||
echo " --run-go-tests Run go tests"
|
||||
echo " --run-go-tests-only Run go tests and exit"
|
||||
echo " --skip-ui-build Skip frontend/UI build"
|
||||
echo " --skip-native-build Skip native build"
|
||||
echo " --disable-docker Disable docker build"
|
||||
echo " --enable-sync-trace Enable sync trace (do not use in release builds)"
|
||||
echo " --native-binary Build and deploy the native binary (FOR DEBUGGING ONLY)"
|
||||
echo " -i, --install Build for release and install the app"
|
||||
echo " --help Display this help message"
|
||||
echo
|
||||
|
|
@ -32,6 +35,9 @@ REMOTE_PATH="/userdata/jetkvm/bin"
|
|||
SKIP_UI_BUILD=false
|
||||
SKIP_UI_BUILD_RELEASE=0
|
||||
SKIP_NATIVE_BUILD=0
|
||||
GDB_DEBUG_PORT=2345
|
||||
BUILD_NATIVE_BINARY=false
|
||||
ENABLE_SYNC_TRACE=0
|
||||
RESET_USB_HID_DEVICE=false
|
||||
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
||||
RUN_GO_TESTS=false
|
||||
|
|
@ -52,6 +58,10 @@ while [[ $# -gt 0 ]]; do
|
|||
REMOTE_USER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--gdb-port)
|
||||
GDB_DEBUG_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-ui-build)
|
||||
SKIP_UI_BUILD=true
|
||||
shift
|
||||
|
|
@ -81,6 +91,10 @@ while [[ $# -gt 0 ]]; do
|
|||
RUN_GO_TESTS=true
|
||||
shift
|
||||
;;
|
||||
--native-binary)
|
||||
BUILD_NATIVE_BINARY=true
|
||||
shift
|
||||
;;
|
||||
-i|--install)
|
||||
INSTALL_APP=true
|
||||
shift
|
||||
|
|
@ -106,6 +120,14 @@ if [ -z "$REMOTE_HOST" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Check device connectivity before proceeding
|
||||
check_ping "${REMOTE_HOST}"
|
||||
check_ssh "${REMOTE_USER}" "${REMOTE_HOST}"
|
||||
function sshdev() {
|
||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "$@"
|
||||
return $?
|
||||
}
|
||||
|
||||
# check if the current CPU architecture is x86_64
|
||||
if [ "$(uname -m)" != "x86_64" ]; then
|
||||
msg_warn "Warning: This script is only supported on x86_64 architecture"
|
||||
|
|
@ -116,6 +138,34 @@ if [ "$BUILD_IN_DOCKER" = true ]; then
|
|||
build_docker_image
|
||||
fi
|
||||
|
||||
if [ "$BUILD_NATIVE_BINARY" = true ]; then
|
||||
msg_info "▶ Building native binary"
|
||||
CMAKE_BUILD_TYPE=Debug make build_native
|
||||
msg_info "▶ Checking if GDB is available on remote host"
|
||||
if ! sshdev "command -v gdbserver > /dev/null 2>&1"; then
|
||||
msg_warn "Error: gdbserver is not installed on the remote host"
|
||||
tar -czf - -C /opt/jetkvm-native-buildkit/gdb/ . | sshdev "tar -xzf - -C /usr/bin"
|
||||
msg_info "✓ gdbserver installed on remote host"
|
||||
fi
|
||||
msg_info "▶ Stopping any existing instances of jetkvm_native_debug on remote host"
|
||||
sshdev "killall -9 jetkvm_app jetkvm_app_debug jetkvm_native_debug gdbserver || true >> /dev/null 2>&1"
|
||||
sshdev "cat > ${REMOTE_PATH}/jetkvm_native_debug" < internal/native/cgo/build/jknative-bin
|
||||
sshdev -t ash << EOF
|
||||
set -e
|
||||
|
||||
# Set the library path to include the directory where librockit.so is located
|
||||
export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH
|
||||
|
||||
cd ${REMOTE_PATH}
|
||||
killall -9 jetkvm_app jetkvm_app_debug jetkvm_native_debug || true
|
||||
sleep 5
|
||||
echo 'V' > /dev/watchdog
|
||||
chmod +x jetkvm_native_debug
|
||||
gdbserver localhost:${GDB_DEBUG_PORT} ./jetkvm_native_debug
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build the development version on the host
|
||||
# When using `make build_release`, the frontend will be built regardless of the `SKIP_UI_BUILD` flag
|
||||
# check if static/index.html exists
|
||||
|
|
@ -140,10 +190,10 @@ if [ "$RUN_GO_TESTS" = true ]; then
|
|||
make build_dev_test
|
||||
|
||||
msg_info "▶ Copying device-tests.tar.gz to remote host"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
||||
sshdev "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
||||
|
||||
msg_info "▶ Running go tests"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
|
||||
sshdev ash << 'EOF'
|
||||
set -e
|
||||
TMP_DIR=$(mktemp -d)
|
||||
cd ${TMP_DIR}
|
||||
|
|
@ -183,30 +233,30 @@ then
|
|||
do_make build_release SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE}
|
||||
|
||||
# Copy the binary to the remote host as if we were the OTA updater.
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
|
||||
|
||||
sshdev "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
|
||||
|
||||
# Reboot the device, the new app will be deployed by the startup process.
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
|
||||
sshdev "reboot"
|
||||
else
|
||||
msg_info "▶ Building development binary"
|
||||
do_make build_dev SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE}
|
||||
|
||||
# Kill any existing instances of the application
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||
|
||||
sshdev "killall jetkvm_app_debug || true"
|
||||
|
||||
# Copy the binary to the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||
|
||||
sshdev "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||
|
||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
||||
msg_info "▶ Resetting USB HID device"
|
||||
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
|
||||
# Remove the old USB gadget configuration
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
sshdev "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
sshdev "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
fi
|
||||
|
||||
# Deploy and run the application on the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||
sshdev ash << EOF
|
||||
set -e
|
||||
|
||||
# Set the library path to include the directory where librockit.so is located
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
#!/bin/bash
|
||||
# Generate gRPC code from proto files
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Check if protoc is installed
|
||||
if ! command -v protoc &> /dev/null; then
|
||||
echo "Error: protoc is not installed"
|
||||
echo "Install it with:"
|
||||
echo " apt-get install protobuf-compiler # Debian/Ubuntu"
|
||||
echo " brew install protobuf # macOS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if protoc-gen-go is installed
|
||||
if ! command -v protoc-gen-go &> /dev/null; then
|
||||
echo "Error: protoc-gen-go is not installed"
|
||||
echo "Install it with: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if protoc-gen-go-grpc is installed
|
||||
if ! command -v protoc-gen-go-grpc &> /dev/null; then
|
||||
echo "Error: protoc-gen-go-grpc is not installed"
|
||||
echo "Install it with: go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate code
|
||||
echo "Generating gRPC code from proto files..."
|
||||
protoc \
|
||||
--go_out=. \
|
||||
--go_opt=paths=source_relative \
|
||||
--go-grpc_out=. \
|
||||
--go-grpc_opt=paths=source_relative \
|
||||
internal/native/proto/native.proto
|
||||
|
||||
echo "Done!"
|
||||
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
#!/bin/bash
|
||||
set -eE
|
||||
set -o pipefail
|
||||
|
||||
SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))")
|
||||
source ${SCRIPT_PATH}/build_utils.sh
|
||||
|
||||
# Function to display help message
|
||||
show_help() {
|
||||
echo "Usage: $0 [options] -v <version>"
|
||||
echo
|
||||
echo "Required:"
|
||||
echo " --app-version <version> App version to release"
|
||||
echo " --system-version <version> System version to release"
|
||||
echo
|
||||
echo "Optional:"
|
||||
echo " -u, --user <remote_user> Remote username (default: root)"
|
||||
echo " --run-go-tests Run go tests"
|
||||
echo " --run-go-tests-only Run go tests and exit"
|
||||
echo " --skip-ui-build Skip frontend/UI build"
|
||||
echo " --skip-native-build Skip native build"
|
||||
echo " --disable-docker Disable docker build"
|
||||
echo " -i, --install Build for release and install the app"
|
||||
echo " --help Display this help message"
|
||||
echo
|
||||
echo "Example:"
|
||||
echo " $0 --system-version 0.2.6"
|
||||
}
|
||||
|
||||
|
||||
BUILD_VERSION=$1
|
||||
R2_PATH="r2://jetkvm-update/system"
|
||||
PACK_BIN_PATH="./tools/linux/Linux_Pack_Firmware"
|
||||
UNPACK_BIN="${PACK_BIN_PATH}/mk-update_unpack.sh"
|
||||
|
||||
# Create temporary directory for downloads
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
msg_ok "Created temporary directory: $TEMP_DIR"
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
if [ -d "$TEMP_DIR" ]; then
|
||||
msg_info "Cleaning up temporary directory: $TEMP_DIR"
|
||||
rm -rf "$TEMP_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# Set trap to cleanup on exit
|
||||
# trap cleanup EXIT
|
||||
|
||||
mkdir -p ${TEMP_DIR}/extracted-update
|
||||
${UNPACK_BIN} -i update.img -o ${TEMP_DIR}/extracted-update
|
||||
|
||||
exit 0
|
||||
# Check if the version already exists
|
||||
if rclone lsf $R2_PATH/$BUILD_VERSION/ | grep -q .; then
|
||||
msg_err "Error: Version $BUILD_VERSION already exists in the remote storage."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the version exists in the github
|
||||
RELEASE_URL="https://api.github.com/repos/jetkvm/rv1106-system/releases/tags/v$BUILD_VERSION"
|
||||
|
||||
# Download the release JSON
|
||||
RELEASE_JSON=$(curl -s $RELEASE_URL)
|
||||
|
||||
# Check if the release has assets we need
|
||||
if echo $RELEASE_JSON | jq -e '.assets | length == 0' > /dev/null; then
|
||||
msg_err "Error: Version $BUILD_VERSION does not have assets we need."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function get_file_by_name() {
|
||||
local file_name=$1
|
||||
local file_url=$(echo $RELEASE_JSON | jq -r ".assets[] | select(.name == \"$file_name\") | .browser_download_url")
|
||||
if [ -z "$file_url" ]; then
|
||||
msg_err "Error: File $file_name not found in the release."
|
||||
exit 1
|
||||
fi
|
||||
local digest=$(echo $RELEASE_JSON | jq -r ".assets[] | select(.name == \"$file_name\") | .digest")
|
||||
local temp_file_path="$TEMP_DIR/$file_name"
|
||||
|
||||
msg_info "Downloading $file_name: $file_url"
|
||||
|
||||
# Download the file to temporary directory
|
||||
curl -L -o "$temp_file_path" "$file_url"
|
||||
|
||||
# Verify digest if available
|
||||
if [ "$digest" != "null" ] && [ -n "$digest" ]; then
|
||||
msg_info "Verifying digest for $file_name ..."
|
||||
local calculated_digest=$(sha256sum "$temp_file_path" | cut -d' ' -f1)
|
||||
# Strip "sha256:" prefix if present
|
||||
local expected_digest=$(echo "$digest" | sed 's/^sha256://')
|
||||
if [ "$calculated_digest" != "$expected_digest" ]; then
|
||||
msg_err "🙅 Digest verification failed for $file_name"
|
||||
msg_info "Expected: $expected_digest"
|
||||
msg_info "Calculated: $calculated_digest"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
msg_warn "Warning: No digest available for $file_name, skipping verification"
|
||||
fi
|
||||
|
||||
msg_ok "✅ $file_name downloaded and verified."
|
||||
}
|
||||
|
||||
get_file_by_name "update_ota.tar"
|
||||
get_file_by_name "update.img"
|
||||
|
||||
strings -d bin/jetkvm_app | grep -x '0.4.8'
|
||||
|
||||
# Ask for confirmation
|
||||
msg_info "Do you want to continue with the release? (y/n)"
|
||||
read -n 1 -s -r -p "Press y to continue, any other key to exit"
|
||||
echo -ne "\n"
|
||||
if [ "$REPLY" != "y" ]; then
|
||||
msg_err "🙅 Release cancelled."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg_info "Releasing $BUILD_VERSION..."
|
||||
|
||||
sha256sum $TEMP_DIR/update_ota.tar | awk '{print $1}' > $TEMP_DIR/update_ota.tar.sha256
|
||||
sha256sum $TEMP_DIR/update.img | awk '{print $1}' > $TEMP_DIR/update.img.sha256
|
||||
|
||||
# Check if the version already exists
|
||||
msg_info "Copying to $R2_PATH/$BUILD_VERSION/"
|
||||
|
||||
rclone copyto --progress $TEMP_DIR/update_ota.tar $R2_PATH/$BUILD_VERSION/system.tar
|
||||
rclone copyto --progress $TEMP_DIR/update_ota.tar.sha256 $R2_PATH/$BUILD_VERSION/system.tar.sha256
|
||||
rclone copyto --progress $TEMP_DIR/update.img $R2_PATH/$BUILD_VERSION/update.img
|
||||
rclone copyto --progress $TEMP_DIR/update.img.sha256 $R2_PATH/$BUILD_VERSION/update.img.sha256
|
||||
|
||||
msg_ok "✅ $BUILD_VERSION released."
|
||||
Loading…
Reference in New Issue