feat: add supervisor and error dump

This commit is contained in:
Siyuan 2025-09-26 12:33:16 +00:00
parent 83b0c7985d
commit 713becd53d
9 changed files with 198 additions and 4 deletions

View File

@ -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
}
}

5
go.mod
View File

@ -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

12
go.sum
View File

@ -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=

View File

@ -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;
}

View File

@ -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);

View File

@ -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()
}

View File

@ -122,3 +122,7 @@ func videoSetEDID(edid string) error {
panicPlatformNotSupported()
return nil
}
func crash() {
panicPlatformNotSupported()
}

View File

@ -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()
}

View File

@ -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()
}
}