From 713becd53db9b6dcf3b613b2dddcc0a346a3772c Mon Sep 17 00:00:00 2001 From: Siyuan Date: Fri, 26 Sep 2025 12:33:16 +0000 Subject: [PATCH] feat: add supervisor and error dump --- cmd/main.go | 151 +++++++++++++++++++++++++++++++- go.mod | 5 ++ go.sum | 12 +++ internal/native/cgo/ctrl.c | 6 ++ internal/native/cgo/ctrl.h | 1 + internal/native/cgo_linux.go | 6 ++ internal/native/cgo_notlinux.go | 4 + internal/native/native.go | 12 +++ native.go | 5 ++ 9 files changed, 198 insertions(+), 4 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 2292bd96..59033c47 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,18 +3,37 @@ package main import ( "flag" "fmt" + "io" "os" + "os/exec" + "os/signal" + "path/filepath" + "syscall" + "time" + "github.com/erikdubbelboer/gspt" "github.com/jetkvm/kvm" ) +const ( + envChildID = "JETKVM_CHILD_ID" + errorDumpDir = "/userdata/jetkvm/" + errorDumpStateFile = ".has_error_dump" + errorDumpTemplate = "jetkvm-%s.log" +) + +func program() { + gspt.SetProcTitle(os.Args[0] + " [app]") + kvm.Main() +} + func main() { versionPtr := flag.Bool("version", false, "print version and exit") - versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit") + versionJSONPtr := flag.Bool("version-json", false, "print version as json and exit") flag.Parse() - if *versionPtr || *versionJsonPtr { - versionData, err := kvm.GetVersionData(*versionJsonPtr) + if *versionPtr || *versionJSONPtr { + versionData, err := kvm.GetVersionData(*versionJSONPtr) if err != nil { fmt.Printf("failed to get version data: %v\n", err) os.Exit(1) @@ -23,5 +42,129 @@ func main() { return } - kvm.Main() + childID := os.Getenv(envChildID) + switch childID { + case "": + doSupervise() + case kvm.GetBuiltAppVersion(): + program() + default: + fmt.Printf("Invalid build version: %s != %s\n", childID, kvm.GetBuiltAppVersion()) + os.Exit(1) + } +} + +func supervise() error { + // check binary path + binPath, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + + // check if binary is same as current binary + if info, statErr := os.Stat(binPath); statErr != nil { + return fmt.Errorf("failed to get executable info: %w", statErr) + // check if binary is empty + } else if info.Size() == 0 { + return fmt.Errorf("binary is empty") + // check if it's executable + } else if info.Mode().Perm()&0111 == 0 { + return fmt.Errorf("binary is not executable") + } + // run the child binary + cmd := exec.Command(binPath) + + cmd.Env = append(os.Environ(), []string{envChildID + "=" + kvm.GetBuiltAppVersion()}...) + cmd.Args = os.Args + + logFile, err := os.CreateTemp("", "jetkvm-stdout.log") + defer func() { + // we don't care about the errors here + _ = logFile.Close() + _ = os.Remove(logFile.Name()) + }() + if err != nil { + return fmt.Errorf("failed to create log file: %w", err) + } + + // Use io.MultiWriter to write to both the original streams and our buffers + cmd.Stdout = io.MultiWriter(os.Stdout, logFile) + cmd.Stderr = io.MultiWriter(os.Stderr, logFile) + if startErr := cmd.Start(); startErr != nil { + return fmt.Errorf("failed to start command: %w", startErr) + } + + go func() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGTERM) + + sig := <-sigChan + _ = cmd.Process.Signal(sig) + }() + + gspt.SetProcTitle(os.Args[0] + " [sup]") + + cmdErr := cmd.Wait() + if cmdErr == nil { + return nil + } + + if exiterr, ok := cmdErr.(*exec.ExitError); ok { + createErrorDump(logFile) + os.Exit(exiterr.ExitCode()) + } + + return nil +} + +func createErrorDump(logFile *os.File) { + logFile.Close() + + // touch the error dump state file + if err := os.WriteFile(filepath.Join(errorDumpDir, errorDumpStateFile), []byte{}, 0644); err != nil { + return + } + + fileName := fmt.Sprintf(errorDumpTemplate, time.Now().Format("20060102150405")) + filePath := filepath.Join(errorDumpDir, fileName) + if err := os.Rename(logFile.Name(), filePath); err == nil { + fmt.Printf("error dump created: %s\n", filePath) + return + } + + fnSrc, err := os.Open(logFile.Name()) + if err != nil { + return + } + defer fnSrc.Close() + + fnDst, err := os.Create(filePath) + if err != nil { + return + } + defer fnDst.Close() + + buf := make([]byte, 1024*1024) + for { + n, err := fnSrc.Read(buf) + if err != nil && err != io.EOF { + return + } + if n == 0 { + break + } + + if _, err := fnDst.Write(buf[:n]); err != nil { + return + } + } + + fmt.Printf("error dump created: %s\n", filePath) +} + +func doSupervise() { + err := supervise() + if err == nil { + return + } } diff --git a/go.mod b/go.mod index a32be340..83ba000e 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b require ( + github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect @@ -45,15 +46,19 @@ require ( github.com/cloudwego/base64x v0.1.5 // indirect github.com/creack/goselect v0.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-jose/go-jose/v4 v4.1.0 // indirect + github.com/go-ole/go-ole v1.2.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/jpillora/overseer v1.1.6 // indirect + github.com/jpillora/s3 v1.1.4 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/go.sum b/go.sum index f6fa893f..33fe2fbc 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= @@ -32,6 +34,8 @@ github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377 h1:gT+RM6gdTIAzMT7HUvmT5mL8SyG8Wx7iS3+L0V34Km4= +github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377/go.mod h1:v6o7m/E9bfvm79dE1iFiF+3T7zLBnrjYjkWMa1J+Hv0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= @@ -46,6 +50,8 @@ github.com/go-co-op/gocron/v2 v2.16.5 h1:j228Jxk7bb9CF8LKR3gS+bK3rcjRUINjlVI+ZMp github.com/go-co-op/gocron/v2 v2.16.5/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc= github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= +github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -70,6 +76,10 @@ github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+m github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/jpillora/overseer v1.1.6 h1:3ygYfNcR3FfOr22miu3vR1iQcXKMHbmULBh98rbkIyo= +github.com/jpillora/overseer v1.1.6/go.mod h1:aPXQtxuVb9PVWRWTXpo+LdnC/YXQ0IBLNXqKMJmgk88= +github.com/jpillora/s3 v1.1.4 h1:YCCKDWzb/Ye9EBNd83ATRF/8wPEy0xd43Rezb6u6fzc= +github.com/jpillora/s3 v1.1.4/go.mod h1:yedE603V+crlFi1Kl/5vZJaBu9pUzE9wvKegU/lF2zs= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -160,6 +170,8 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/native/cgo/ctrl.c b/internal/native/cgo/ctrl.c index 930b8514..dd285859 100644 --- a/internal/native/cgo/ctrl.c +++ b/internal/native/cgo/ctrl.c @@ -411,4 +411,10 @@ int jetkvm_video_init() { void jetkvm_video_shutdown() { video_shutdown(); +} + +void jetkvm_crash() { + // let's call a function that will crash the program + int* p = 0; + *p = 0; } \ No newline at end of file diff --git a/internal/native/cgo/ctrl.h b/internal/native/cgo/ctrl.h index a87070a0..430e4c21 100644 --- a/internal/native/cgo/ctrl.h +++ b/internal/native/cgo/ctrl.h @@ -26,6 +26,7 @@ void jetkvm_set_indev_handler(jetkvm_indev_handler_t *handler); void jetkvm_set_rpc_handler(jetkvm_rpc_handler_t *handler); void jetkvm_call_rpc_handler(const char *method, const char *params); void jetkvm_set_video_state_handler(jetkvm_video_state_handler_t *handler); +void jetkvm_crash(); void jetkvm_ui_set_var(const char *name, const char *value); const char *jetkvm_ui_get_var(const char *name); diff --git a/internal/native/cgo_linux.go b/internal/native/cgo_linux.go index 4356f2d6..77b7d74f 100644 --- a/internal/native/cgo_linux.go +++ b/internal/native/cgo_linux.go @@ -389,3 +389,9 @@ func videoSetEDID(edid string) error { C.jetkvm_video_set_edid(edidCStr) return nil } + +// DO NOT USE THIS FUNCTION IN PRODUCTION +// This is only for testing purposes +func crash() { + C.jetkvm_crash() +} diff --git a/internal/native/cgo_notlinux.go b/internal/native/cgo_notlinux.go index 6dc14c9c..4602f713 100644 --- a/internal/native/cgo_notlinux.go +++ b/internal/native/cgo_notlinux.go @@ -122,3 +122,7 @@ func videoSetEDID(edid string) error { panicPlatformNotSupported() return nil } + +func crash() { + panicPlatformNotSupported() +} diff --git a/internal/native/native.go b/internal/native/native.go index 55e16346..69551b9b 100644 --- a/internal/native/native.go +++ b/internal/native/native.go @@ -99,3 +99,15 @@ func (n *Native) Start() { close(n.ready) } + +// DoNotUseThisIsForCrashTestingOnly +// will crash the program in cgo code +func (n *Native) DoNotUseThisIsForCrashTestingOnly() { + defer func() { + if r := recover(); r != nil { + n.l.Error().Msg("recovered from crash") + } + }() + + crash() +} diff --git a/native.go b/native.go index 0cf9ed4c..e8eea745 100644 --- a/native.go +++ b/native.go @@ -1,6 +1,7 @@ package kvm import ( + "os" "sync" "time" @@ -56,4 +57,8 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) { }, }) nativeInstance.Start() + + if os.Getenv("JETKVM_CRASH_TESTING") == "1" { + nativeInstance.DoNotUseThisIsForCrashTestingOnly() + } }