mirror of https://github.com/jetkvm/kvm.git
Compare commits
7 Commits
6afb15b29a
...
502cd4ecc2
| Author | SHA1 | Date |
|---|---|---|
|
|
502cd4ecc2 | |
|
|
5933adb23b | |
|
|
82ad2a467f | |
|
|
72f29df835 | |
|
|
99a60120e5 | |
|
|
03ab8d8285 | |
|
|
239c2dc932 |
118
cmd/main.go
118
cmd/main.go
|
|
@ -16,10 +16,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
envChildID = "JETKVM_CHILD_ID"
|
envChildID = "JETKVM_CHILD_ID"
|
||||||
errorDumpDir = "/userdata/jetkvm/"
|
errorDumpDir = "/userdata/jetkvm/crashdump"
|
||||||
errorDumpStateFile = ".has_error_dump"
|
errorDumpLastFile = "last-crash.log"
|
||||||
errorDumpTemplate = "jetkvm-%s.log"
|
errorDumpTemplate = "jetkvm-%s.log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func program() {
|
func program() {
|
||||||
|
|
@ -74,7 +74,12 @@ func supervise() error {
|
||||||
// run the child binary
|
// run the child binary
|
||||||
cmd := exec.Command(binPath)
|
cmd := exec.Command(binPath)
|
||||||
|
|
||||||
cmd.Env = append(os.Environ(), []string{envChildID + "=" + kvm.GetBuiltAppVersion()}...)
|
lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile)
|
||||||
|
|
||||||
|
cmd.Env = append(os.Environ(), []string{
|
||||||
|
fmt.Sprintf("%s=%s", envChildID, kvm.GetBuiltAppVersion()),
|
||||||
|
fmt.Sprintf("JETKVM_LAST_ERROR_PATH=%s", lastFilePath),
|
||||||
|
}...)
|
||||||
cmd.Args = os.Args
|
cmd.Args = os.Args
|
||||||
|
|
||||||
logFile, err := os.CreateTemp("", "jetkvm-stdout.log")
|
logFile, err := os.CreateTemp("", "jetkvm-stdout.log")
|
||||||
|
|
@ -117,30 +122,47 @@ func supervise() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createErrorDump(logFile *os.File) {
|
func isSymlinkTo(oldName, newName string) bool {
|
||||||
logFile.Close()
|
file, err := os.Stat(newName)
|
||||||
|
|
||||||
// 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 {
|
if err != nil {
|
||||||
return
|
return false
|
||||||
|
}
|
||||||
|
if file.Mode()&os.ModeSymlink != os.ModeSymlink {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
target, err := os.Readlink(newName)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return target == oldName
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureSymlink(oldName, newName string) error {
|
||||||
|
if isSymlinkTo(oldName, newName) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = os.Remove(newName)
|
||||||
|
return os.Symlink(oldName, newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renameFile(f *os.File, newName string) error {
|
||||||
|
_ = f.Close()
|
||||||
|
|
||||||
|
// try to rename the file first
|
||||||
|
if err := os.Rename(f.Name(), newName); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy the log file to the error dump directory
|
||||||
|
fnSrc, err := os.Open(f.Name())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open file: %w", err)
|
||||||
}
|
}
|
||||||
defer fnSrc.Close()
|
defer fnSrc.Close()
|
||||||
|
|
||||||
fnDst, err := os.Create(filePath)
|
fnDst, err := os.Create(newName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return fmt.Errorf("failed to create file: %w", err)
|
||||||
}
|
}
|
||||||
defer fnDst.Close()
|
defer fnDst.Close()
|
||||||
|
|
||||||
|
|
@ -148,18 +170,60 @@ func createErrorDump(logFile *os.File) {
|
||||||
for {
|
for {
|
||||||
n, err := fnSrc.Read(buf)
|
n, err := fnSrc.Read(buf)
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
return
|
return fmt.Errorf("failed to read file: %w", err)
|
||||||
}
|
}
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fnDst.Write(buf[:n]); err != nil {
|
if _, err := fnDst.Write(buf[:n]); err != nil {
|
||||||
return
|
return fmt.Errorf("failed to write file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("error dump created: %s\n", filePath)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureErrorDumpDir() error {
|
||||||
|
// TODO: check if the directory is writable
|
||||||
|
f, err := os.Stat(errorDumpDir)
|
||||||
|
if err == nil && f.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(errorDumpDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create error dump directory: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createErrorDump(logFile *os.File) {
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fileName := fmt.Sprintf(
|
||||||
|
errorDumpTemplate,
|
||||||
|
time.Now().Format("20060102-150405"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// check if the directory exists
|
||||||
|
if err := ensureErrorDumpDir(); err != nil {
|
||||||
|
fmt.Printf("failed to ensure error dump directory: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(errorDumpDir, fileName)
|
||||||
|
if err := renameFile(logFile, filePath); err != nil {
|
||||||
|
fmt.Printf("failed to rename file: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("error dump copied: %s\n", filePath)
|
||||||
|
|
||||||
|
lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile)
|
||||||
|
|
||||||
|
if err := ensureSymlink(filePath, lastFilePath); err != nil {
|
||||||
|
fmt.Printf("failed to create symlink: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func doSupervise() {
|
func doSupervise() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
package kvm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
failsafeDefaultLastCrashPath = "/userdata/jetkvm/crashdump/last-crash.log"
|
||||||
|
failsafeFile = "/userdata/jetkvm/.enablefailsafe"
|
||||||
|
failsafeLastCrashEnv = "JETKVM_LAST_ERROR_PATH"
|
||||||
|
failsafeEnv = "JETKVM_FORCE_FAILSAFE"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
failsafeOnce sync.Once
|
||||||
|
failsafeCrashLog = ""
|
||||||
|
failsafeModeActive = false
|
||||||
|
failsafeModeReason = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
type FailsafeModeNotification struct {
|
||||||
|
Active bool `json:"active"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function has side effects and can be only executed once
|
||||||
|
func checkFailsafeReason() {
|
||||||
|
failsafeOnce.Do(func() {
|
||||||
|
// check if the failsafe environment variable is set
|
||||||
|
if os.Getenv(failsafeEnv) == "1" {
|
||||||
|
failsafeModeActive = true
|
||||||
|
failsafeModeReason = "failsafe_env_set"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the failsafe file exists
|
||||||
|
if _, err := os.Stat(failsafeFile); err == nil {
|
||||||
|
failsafeModeActive = true
|
||||||
|
failsafeModeReason = "failsafe_file_exists"
|
||||||
|
_ = os.Remove(failsafeFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the last crash log path from the environment variable
|
||||||
|
lastCrashPath := os.Getenv(failsafeLastCrashEnv)
|
||||||
|
if lastCrashPath == "" {
|
||||||
|
lastCrashPath = failsafeDefaultLastCrashPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the last crash log file exists
|
||||||
|
l := failsafeLogger.With().Str("path", lastCrashPath).Logger()
|
||||||
|
fi, err := os.Lstat(lastCrashPath)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
l.Warn().Err(err).Msg("failed to stat last crash log")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
|
||||||
|
l.Warn().Msg("last crash log is not a symlink, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the last crash log file and find if it contains the string "panic"
|
||||||
|
content, err := os.ReadFile(lastCrashPath)
|
||||||
|
if err != nil {
|
||||||
|
l.Warn().Err(err).Msg("failed to read last crash log")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlink the last crash log file
|
||||||
|
failsafeCrashLog = string(content)
|
||||||
|
_ = os.Remove(lastCrashPath)
|
||||||
|
|
||||||
|
// TODO: read the goroutine stack trace and check which goroutine is panicking
|
||||||
|
if strings.Contains(failsafeCrashLog, "runtime.cgocall") {
|
||||||
|
failsafeModeActive = true
|
||||||
|
failsafeModeReason = "video"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func notifyFailsafeMode(session *Session) {
|
||||||
|
if !failsafeModeActive || session == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonRpcLogger.Info().Str("reason", failsafeModeReason).Msg("sending failsafe mode notification")
|
||||||
|
|
||||||
|
writeJSONRPCEvent("failsafeMode", FailsafeModeNotification{
|
||||||
|
Active: true,
|
||||||
|
Reason: failsafeModeReason,
|
||||||
|
}, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetFailsafeLogs() (string, error) {
|
||||||
|
if !failsafeModeActive {
|
||||||
|
return "", fmt.Errorf("failsafe mode is not active")
|
||||||
|
}
|
||||||
|
|
||||||
|
return failsafeCrashLog, nil
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
//go:build linux
|
//go:build linux
|
||||||
|
|
||||||
|
// TODO: use a generator to generate the cgo code for the native functions
|
||||||
|
// there's too much boilerplate code to write manually
|
||||||
|
|
||||||
package native
|
package native
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -46,7 +49,17 @@ static inline void jetkvm_cgo_setup_rpc_handler() {
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
var cgoLock sync.Mutex
|
var (
|
||||||
|
cgoLock sync.Mutex
|
||||||
|
cgoDisabled bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func setCgoDisabled(disabled bool) {
|
||||||
|
cgoLock.Lock()
|
||||||
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
cgoDisabled = disabled
|
||||||
|
}
|
||||||
|
|
||||||
//export jetkvm_go_video_state_handler
|
//export jetkvm_go_video_state_handler
|
||||||
func jetkvm_go_video_state_handler(state *C.jetkvm_video_state_t) {
|
func jetkvm_go_video_state_handler(state *C.jetkvm_video_state_t) {
|
||||||
|
|
@ -91,6 +104,10 @@ func jetkvm_go_rpc_handler(method *C.cchar_t, params *C.cchar_t) {
|
||||||
var eventCodeToNameMap = map[int]string{}
|
var eventCodeToNameMap = map[int]string{}
|
||||||
|
|
||||||
func uiEventCodeToName(code int) string {
|
func uiEventCodeToName(code int) string {
|
||||||
|
if cgoDisabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
name, ok := eventCodeToNameMap[code]
|
name, ok := eventCodeToNameMap[code]
|
||||||
if !ok {
|
if !ok {
|
||||||
cCode := C.int(code)
|
cCode := C.int(code)
|
||||||
|
|
@ -103,6 +120,10 @@ func uiEventCodeToName(code int) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setUpNativeHandlers() {
|
func setUpNativeHandlers() {
|
||||||
|
if cgoDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -114,6 +135,10 @@ func setUpNativeHandlers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiInit(rotation uint16) {
|
func uiInit(rotation uint16) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -123,6 +148,10 @@ func uiInit(rotation uint16) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiTick() {
|
func uiTick() {
|
||||||
|
if cgoDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -130,6 +159,10 @@ func uiTick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoInit(factor float64) error {
|
func videoInit(factor float64) error {
|
||||||
|
if cgoDisabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -143,6 +176,10 @@ func videoInit(factor float64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoShutdown() {
|
func videoShutdown() {
|
||||||
|
if cgoDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -150,6 +187,10 @@ func videoShutdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoStart() {
|
func videoStart() {
|
||||||
|
if cgoDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -157,6 +198,10 @@ func videoStart() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoStop() {
|
func videoStop() {
|
||||||
|
if cgoDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -164,6 +209,10 @@ func videoStop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoLogStatus() string {
|
func videoLogStatus() string {
|
||||||
|
if cgoDisabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -174,6 +223,10 @@ func videoLogStatus() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiSetVar(name string, value string) {
|
func uiSetVar(name string, value string) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -187,6 +240,10 @@ func uiSetVar(name string, value string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiGetVar(name string) string {
|
func uiGetVar(name string) string {
|
||||||
|
if cgoDisabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -197,6 +254,10 @@ func uiGetVar(name string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiSwitchToScreen(screen string) {
|
func uiSwitchToScreen(screen string) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -206,6 +267,10 @@ func uiSwitchToScreen(screen string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiGetCurrentScreen() string {
|
func uiGetCurrentScreen() string {
|
||||||
|
if cgoDisabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -214,6 +279,10 @@ func uiGetCurrentScreen() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiObjAddState(objName string, state string) (bool, error) {
|
func uiObjAddState(objName string, state string) (bool, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -226,6 +295,10 @@ func uiObjAddState(objName string, state string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiObjClearState(objName string, state string) (bool, error) {
|
func uiObjClearState(objName string, state string) (bool, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -238,6 +311,10 @@ func uiObjClearState(objName string, state string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiGetLVGLVersion() string {
|
func uiGetLVGLVersion() string {
|
||||||
|
if cgoDisabled {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -246,6 +323,10 @@ func uiGetLVGLVersion() string {
|
||||||
|
|
||||||
// TODO: use Enum instead of string but it's not a hot path and performance is not a concern now
|
// 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) {
|
func uiObjAddFlag(objName string, flag string) (bool, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -258,6 +339,10 @@ func uiObjAddFlag(objName string, flag string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiObjClearFlag(objName string, flag string) (bool, error) {
|
func uiObjClearFlag(objName string, flag string) (bool, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -278,6 +363,10 @@ func uiObjShow(objName string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiObjSetOpacity(objName string, opacity int) (bool, error) {
|
func uiObjSetOpacity(objName string, opacity int) (bool, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -289,6 +378,10 @@ func uiObjSetOpacity(objName string, opacity int) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiObjFadeIn(objName string, duration uint32) (bool, error) {
|
func uiObjFadeIn(objName string, duration uint32) (bool, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -301,6 +394,10 @@ func uiObjFadeIn(objName string, duration uint32) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiObjFadeOut(objName string, duration uint32) (bool, error) {
|
func uiObjFadeOut(objName string, duration uint32) (bool, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -313,6 +410,10 @@ func uiObjFadeOut(objName string, duration uint32) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiLabelSetText(objName string, text string) (bool, error) {
|
func uiLabelSetText(objName string, text string) (bool, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -330,6 +431,10 @@ func uiLabelSetText(objName string, text string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiImgSetSrc(objName string, src string) (bool, error) {
|
func uiImgSetSrc(objName string, src string) (bool, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -345,6 +450,10 @@ func uiImgSetSrc(objName string, src string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uiDispSetRotation(rotation uint16) (bool, error) {
|
func uiDispSetRotation(rotation uint16) (bool, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -357,6 +466,10 @@ func uiDispSetRotation(rotation uint16) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoGetStreamQualityFactor() (float64, error) {
|
func videoGetStreamQualityFactor() (float64, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -365,6 +478,10 @@ func videoGetStreamQualityFactor() (float64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoSetStreamQualityFactor(factor float64) error {
|
func videoSetStreamQualityFactor(factor float64) error {
|
||||||
|
if cgoDisabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -373,6 +490,10 @@ func videoSetStreamQualityFactor(factor float64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoGetEDID() (string, error) {
|
func videoGetEDID() (string, error) {
|
||||||
|
if cgoDisabled {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
@ -381,6 +502,10 @@ func videoGetEDID() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoSetEDID(edid string) error {
|
func videoSetEDID(edid string) error {
|
||||||
|
if cgoDisabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
cgoLock.Lock()
|
cgoLock.Lock()
|
||||||
defer cgoLock.Unlock()
|
defer cgoLock.Unlock()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Native struct {
|
type Native struct {
|
||||||
|
disable bool
|
||||||
ready chan struct{}
|
ready chan struct{}
|
||||||
l *zerolog.Logger
|
l *zerolog.Logger
|
||||||
lD *zerolog.Logger
|
lD *zerolog.Logger
|
||||||
|
|
@ -27,6 +28,7 @@ type Native struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type NativeOptions struct {
|
type NativeOptions struct {
|
||||||
|
Disable bool
|
||||||
SystemVersion *semver.Version
|
SystemVersion *semver.Version
|
||||||
AppVersion *semver.Version
|
AppVersion *semver.Version
|
||||||
DisplayRotation uint16
|
DisplayRotation uint16
|
||||||
|
|
@ -74,6 +76,7 @@ func NewNative(opts NativeOptions) *Native {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Native{
|
return &Native{
|
||||||
|
disable: opts.Disable,
|
||||||
ready: make(chan struct{}),
|
ready: make(chan struct{}),
|
||||||
l: nativeLogger,
|
l: nativeLogger,
|
||||||
lD: displayLogger,
|
lD: displayLogger,
|
||||||
|
|
@ -92,6 +95,12 @@ func NewNative(opts NativeOptions) *Native {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Native) Start() {
|
func (n *Native) Start() {
|
||||||
|
if n.disable {
|
||||||
|
nativeLogger.Warn().Msg("native is disabled, skipping initialization")
|
||||||
|
setCgoDisabled(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// set up singleton
|
// set up singleton
|
||||||
setInstance(n)
|
setInstance(n)
|
||||||
setUpNativeHandlers()
|
setUpNativeHandlers()
|
||||||
|
|
|
||||||
|
|
@ -1268,4 +1268,5 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
||||||
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
|
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
|
||||||
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
|
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
|
||||||
|
"getFailSafeLogs": {Func: rpcGetFailsafeLogs},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
log.go
1
log.go
|
|
@ -11,6 +11,7 @@ func ErrorfL(l *zerolog.Logger, format string, err error, args ...any) error {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger = logging.GetSubsystemLogger("jetkvm")
|
logger = logging.GetSubsystemLogger("jetkvm")
|
||||||
|
failsafeLogger = logging.GetSubsystemLogger("failsafe")
|
||||||
networkLogger = logging.GetSubsystemLogger("network")
|
networkLogger = logging.GetSubsystemLogger("network")
|
||||||
cloudLogger = logging.GetSubsystemLogger("cloud")
|
cloudLogger = logging.GetSubsystemLogger("cloud")
|
||||||
websocketLogger = logging.GetSubsystemLogger("websocket")
|
websocketLogger = logging.GetSubsystemLogger("websocket")
|
||||||
|
|
|
||||||
7
main.go
7
main.go
|
|
@ -14,6 +14,11 @@ import (
|
||||||
var appCtx context.Context
|
var appCtx context.Context
|
||||||
|
|
||||||
func Main() {
|
func Main() {
|
||||||
|
checkFailsafeReason()
|
||||||
|
if failsafeModeActive {
|
||||||
|
logger.Warn().Str("reason", failsafeModeReason).Msg("failsafe mode activated")
|
||||||
|
}
|
||||||
|
|
||||||
LoadConfig()
|
LoadConfig()
|
||||||
|
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
|
|
@ -48,6 +53,7 @@ func Main() {
|
||||||
// Initialize network
|
// Initialize network
|
||||||
if err := initNetwork(); err != nil {
|
if err := initNetwork(); err != nil {
|
||||||
logger.Error().Err(err).Msg("failed to initialize network")
|
logger.Error().Err(err).Msg("failed to initialize network")
|
||||||
|
// TODO: reset config to default
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +64,6 @@ func Main() {
|
||||||
// Initialize mDNS
|
// Initialize mDNS
|
||||||
if err := initMdns(); err != nil {
|
if err := initMdns(); err != nil {
|
||||||
logger.Error().Err(err).Msg("failed to initialize mDNS")
|
logger.Error().Err(err).Msg("failed to initialize mDNS")
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initPrometheus()
|
initPrometheus()
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ var (
|
||||||
|
|
||||||
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
|
||||||
nativeInstance = native.NewNative(native.NativeOptions{
|
nativeInstance = native.NewNative(native.NativeOptions{
|
||||||
|
Disable: failsafeModeActive,
|
||||||
SystemVersion: systemVersion,
|
SystemVersion: systemVersion,
|
||||||
AppVersion: appVersion,
|
AppVersion: appVersion,
|
||||||
DisplayRotation: config.GetDisplayRotation(),
|
DisplayRotation: config.GetDisplayRotation(),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { LuInfo } from "react-icons/lu";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
import { useVersion } from "@/hooks/useVersion";
|
import { useVersion } from "@/hooks/useVersion";
|
||||||
|
|
@ -30,19 +31,47 @@ function OverlayContent({ children }: OverlayContentProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
readonly children: React.ReactNode;
|
||||||
|
readonly text: string;
|
||||||
|
readonly show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tooltip({ children, text, show }: TooltipProps) {
|
||||||
|
if (!show) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group/tooltip relative">
|
||||||
|
{children}
|
||||||
|
<div className="pointer-events-none absolute bottom-full left-1/2 mb-2 hidden -translate-x-1/2 opacity-0 transition-opacity group-hover/tooltip:block group-hover/tooltip:opacity-100">
|
||||||
|
<Card>
|
||||||
|
<div className="whitespace-nowrap px-2 py-1 text-xs flex items-center gap-1 justify-center">
|
||||||
|
<LuInfo className="h-3 w-3 text-slate-700 dark:text-slate-300" />
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) {
|
export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) {
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
const { appVersion } = useVersion();
|
const { appVersion } = useVersion();
|
||||||
const { systemVersion } = useDeviceStore();
|
const { systemVersion } = useDeviceStore();
|
||||||
const [isDownloadingLogs, setIsDownloadingLogs] = useState(false);
|
const [isDownloadingLogs, setIsDownloadingLogs] = useState(false);
|
||||||
|
const [hasDownloadedLogs, setHasDownloadedLogs] = useState(false);
|
||||||
|
|
||||||
const getReasonCopy = () => {
|
const getReasonCopy = () => {
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case "video":
|
case "video":
|
||||||
return {
|
return {
|
||||||
message:
|
message:
|
||||||
"We've detected an issue with the video capture process. Your device is still running and accessible, but video streaming is temporarily unavailable. You can reboot to attempt recovery, report the issue, or downgrade to the last stable version.",
|
"We've detected an issue with the video capture process. Your device is still running and accessible, but video streaming is temporarily unavailable.",
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
|
|
@ -80,13 +109,14 @@ export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) {
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
notifications.success("Recovery logs downloaded successfully");
|
notifications.success("Crash logs downloaded successfully");
|
||||||
|
setHasDownloadedLogs(true);
|
||||||
|
|
||||||
// Open GitHub issue
|
// Open GitHub issue
|
||||||
const issueBody = `## Issue Description
|
const issueBody = `## Issue Description
|
||||||
The ${reason} process encountered an error and recovery mode was activated.
|
The \`${reason}\` process encountered an error and fail safe mode was activated.
|
||||||
|
|
||||||
**Reason:** ${reason}
|
**Reason:** \`${reason}\`
|
||||||
**Timestamp:** ${new Date().toISOString()}
|
**Timestamp:** ${new Date().toISOString()}
|
||||||
**App Version:** ${appVersion || "Unknown"}
|
**App Version:** ${appVersion || "Unknown"}
|
||||||
**System Version:** ${systemVersion || "Unknown"}
|
**System Version:** ${systemVersion || "Unknown"}
|
||||||
|
|
@ -95,6 +125,9 @@ The ${reason} process encountered an error and recovery mode was activated.
|
||||||
Please attach the recovery logs file that was downloaded to your computer:
|
Please attach the recovery logs file that was downloaded to your computer:
|
||||||
\`${filename}\`
|
\`${filename}\`
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Please omit any sensitive information from the logs.
|
||||||
|
|
||||||
## Additional Context
|
## Additional Context
|
||||||
[Please describe what you were doing when this occurred]`;
|
[Please describe what you were doing when this occurred]`;
|
||||||
|
|
||||||
|
|
@ -108,13 +141,13 @@ Please attach the recovery logs file that was downloaded to your computer:
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDowngrade = () => {
|
const handleDowngrade = () => {
|
||||||
navigateTo("/settings/general/update?appVersion=0.4.8");
|
navigateTo("/settings/general/update?app=0.4.8");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="aspect-video h-full w-full"
|
className="aspect-video h-full w-full isolate"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0, transition: { duration: 0 } }}
|
exit={{ opacity: 0, transition: { duration: 0 } }}
|
||||||
|
|
@ -132,29 +165,40 @@ Please attach the recovery logs file that was downloaded to your computer:
|
||||||
<h2 className="text-xl font-bold">Fail safe mode activated</h2>
|
<h2 className="text-xl font-bold">Fail safe mode activated</h2>
|
||||||
<p className="text-sm">{message}</p>
|
<p className="text-sm">{message}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="space-y-3">
|
||||||
<Button
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
onClick={handleReportAndDownloadLogs}
|
<Button
|
||||||
theme="primary"
|
onClick={handleReportAndDownloadLogs}
|
||||||
size="SM"
|
theme="primary"
|
||||||
disabled={isDownloadingLogs}
|
size="SM"
|
||||||
LeadingIcon={GitHubIcon}
|
disabled={isDownloadingLogs}
|
||||||
text={isDownloadingLogs ? "Downloading Logs..." : "Report Issue & Download Logs"}
|
LeadingIcon={GitHubIcon}
|
||||||
/>
|
text={isDownloadingLogs ? "Downloading Logs..." : "Download Logs & Report Issue"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="h-8 w-px bg-slate-200 dark:bg-slate-700 block" />
|
||||||
|
<Tooltip text="Download logs first to unlock" show={!hasDownloadedLogs}>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigateTo("/settings/general/reboot")}
|
||||||
|
theme="light"
|
||||||
|
size="SM"
|
||||||
|
text="Reboot Device"
|
||||||
|
disabled={!hasDownloadedLogs}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip text="Download logs first to unlock" show={!hasDownloadedLogs}>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
onClick={handleDowngrade}
|
||||||
|
theme="light"
|
||||||
|
text="Downgrade to v0.4.8"
|
||||||
|
disabled={!hasDownloadedLogs}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => navigateTo("/settings/general/reboot")}
|
|
||||||
theme="light"
|
|
||||||
size="SM"
|
|
||||||
text="Reboot Device"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="SM"
|
|
||||||
onClick={handleDowngrade}
|
|
||||||
theme="light"
|
|
||||||
text="Downgrade to v0.4.8"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -930,12 +930,12 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
export interface FailsafeModeState {
|
export interface FailsafeModeState {
|
||||||
isFailsafeMode: boolean;
|
isFailsafeMode: boolean;
|
||||||
reason: string | null; // "video", "network", etc.
|
reason: string | null; // "video", "network", etc.
|
||||||
setFailsafeMode: (enabled: boolean, reason: string | null) => void;
|
setFailsafeMode: (active: boolean, reason: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFailsafeModeStore = create<FailsafeModeState>(set => ({
|
export const useFailsafeModeStore = create<FailsafeModeState>(set => ({
|
||||||
isFailsafeMode: false,
|
isFailsafeMode: false,
|
||||||
reason: null,
|
reason: null,
|
||||||
setFailsafeMode: (enabled: boolean, reason: string | null) =>
|
setFailsafeMode: (active: boolean, reason: string | null) =>
|
||||||
set({ isFailsafeMode: enabled, reason }),
|
set({ isFailsafeMode: active, reason }),
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ const blockedMethodsByReason: Record<string, string[]> = {
|
||||||
|
|
||||||
export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
||||||
const { rpcDataChannel } = useRTCStore();
|
const { rpcDataChannel } = useRTCStore();
|
||||||
const { isFailsafeMode: isFailsafeMode, reason } = useFailsafeModeStore();
|
const { isFailsafeMode, reason } = useFailsafeModeStore();
|
||||||
|
|
||||||
const send = useCallback(
|
const send = useCallback(
|
||||||
async (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => {
|
async (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => {
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,42 @@
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { useCallback } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
|
||||||
|
import LoadingSpinner from "../components/LoadingSpinner";
|
||||||
|
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||||
|
|
||||||
|
// Time to wait after initiating reboot before redirecting to home
|
||||||
|
const REBOOT_REDIRECT_DELAY_MS = 5000;
|
||||||
|
|
||||||
export default function SettingsGeneralRebootRoute() {
|
export default function SettingsGeneralRebootRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
|
const [isRebooting, setIsRebooting] = useState(false);
|
||||||
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
|
||||||
const onConfirmUpdate = useCallback(() => {
|
const onConfirmUpdate = useCallback(async () => {
|
||||||
|
setIsRebooting(true);
|
||||||
// This is where we send the RPC to the golang binary
|
// This is where we send the RPC to the golang binary
|
||||||
send("reboot", {force: true});
|
send("reboot", { force: true });
|
||||||
}, [send]);
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, REBOOT_REDIRECT_DELAY_MS));
|
||||||
|
navigateTo("/");
|
||||||
|
}, [navigateTo, send]);
|
||||||
|
|
||||||
{
|
{
|
||||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||||
}
|
}
|
||||||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
return <Dialog isRebooting={isRebooting} onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
|
isRebooting,
|
||||||
onClose,
|
onClose,
|
||||||
onConfirmUpdate,
|
onConfirmUpdate,
|
||||||
}: {
|
}: {
|
||||||
|
isRebooting: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onConfirmUpdate: () => void;
|
onConfirmUpdate: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -30,19 +44,22 @@ export function Dialog({
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-auto relative mx-auto text-left">
|
<div className="pointer-events-auto relative mx-auto text-left">
|
||||||
<div>
|
<div>
|
||||||
<ConfirmationBox
|
<ConfirmationBox
|
||||||
onYes={onConfirmUpdate}
|
isRebooting={isRebooting}
|
||||||
onNo={onClose}
|
onYes={onConfirmUpdate}
|
||||||
/>
|
onNo={onClose}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConfirmationBox({
|
function ConfirmationBox({
|
||||||
|
isRebooting,
|
||||||
onYes,
|
onYes,
|
||||||
onNo,
|
onNo,
|
||||||
}: {
|
}: {
|
||||||
|
isRebooting: boolean;
|
||||||
onYes: () => void;
|
onYes: () => void;
|
||||||
onNo: () => void;
|
onNo: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -55,11 +72,16 @@ function ConfirmationBox({
|
||||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||||
Do you want to proceed with rebooting the system?
|
Do you want to proceed with rebooting the system?
|
||||||
</p>
|
</p>
|
||||||
|
{isRebooting ? (
|
||||||
<div className="mt-4 flex gap-x-2">
|
<div className="mt-4 flex items-center justify-center">
|
||||||
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
|
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||||
<Button size="SM" theme="blank" text="No" onClick={onNo} />
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div className="mt-4 flex gap-x-2">
|
||||||
|
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
|
||||||
|
<Button size="SM" theme="blank" text="No" onClick={onNo} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ export default function SettingsRoute() {
|
||||||
</div>
|
</div>
|
||||||
</FeatureFlag>
|
</FeatureFlag>
|
||||||
<div className={cx("shrink-0", {
|
<div className={cx("shrink-0", {
|
||||||
"opacity-50 cursor-not-allowed!": isVideoDisabled
|
"opacity-50 cursor-not-allowed": isVideoDisabled
|
||||||
})}>
|
})}>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="video"
|
to="video"
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectio
|
||||||
const Terminal = lazy(() => import('@components/Terminal'));
|
const Terminal = lazy(() => import('@components/Terminal'));
|
||||||
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
|
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import { FailSafeModeOverlay } from "@components/FaileSafeModeOverlay";
|
import { FailSafeModeOverlay } from "@components/FailSafeModeOverlay";
|
||||||
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import {
|
import {
|
||||||
ConnectionFailedOverlay,
|
ConnectionFailedOverlay,
|
||||||
|
|
@ -126,7 +126,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
const params = useParams() as { id: string };
|
const params = useParams() as { id: string };
|
||||||
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
|
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
|
||||||
const [ queryParams, setQueryParams ] = useSearchParams();
|
const [queryParams, setQueryParams] = useSearchParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
peerConnection, setPeerConnection,
|
peerConnection, setPeerConnection,
|
||||||
|
|
@ -483,10 +483,6 @@ export default function KvmIdRoute() {
|
||||||
const rpcDataChannel = pc.createDataChannel("rpc");
|
const rpcDataChannel = pc.createDataChannel("rpc");
|
||||||
rpcDataChannel.onopen = () => {
|
rpcDataChannel.onopen = () => {
|
||||||
setRpcDataChannel(rpcDataChannel);
|
setRpcDataChannel(rpcDataChannel);
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// useFailsafeModeStore.setState({ isFailsafeMode: true, reason: "video" });
|
|
||||||
// }, 1000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const rpcHidChannel = pc.createDataChannel("hidrpc");
|
const rpcHidChannel = pc.createDataChannel("hidrpc");
|
||||||
|
|
@ -604,10 +600,10 @@ export default function KvmIdRoute() {
|
||||||
});
|
});
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
const { setNetworkState} = useNetworkStateStore();
|
const { setNetworkState } = useNetworkStateStore();
|
||||||
const { setHdmiState } = useVideoStore();
|
const { setHdmiState } = useVideoStore();
|
||||||
const {
|
const {
|
||||||
keyboardLedState, setKeyboardLedState,
|
keyboardLedState, setKeyboardLedState,
|
||||||
keysDownState, setKeysDownState, setUsbState,
|
keysDownState, setKeysDownState, setUsbState,
|
||||||
} = useHidStore();
|
} = useHidStore();
|
||||||
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled);
|
||||||
|
|
@ -676,9 +672,9 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.method === "failsafeMode") {
|
if (resp.method === "failsafeMode") {
|
||||||
const { enabled, reason } = resp.params as { enabled: boolean; reason: string };
|
const { active, reason } = resp.params as { active: boolean; reason: string };
|
||||||
console.debug("Setting failsafe mode", { enabled, reason });
|
console.debug("Setting failsafe mode", { active, reason });
|
||||||
setFailsafeMode(enabled, reason);
|
setFailsafeMode(active, reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -770,7 +766,7 @@ export default function KvmIdRoute() {
|
||||||
if (location.pathname !== "/other-session") navigateTo("/");
|
if (location.pathname !== "/other-session") navigateTo("/");
|
||||||
}, [navigateTo, location.pathname]);
|
}, [navigateTo, location.pathname]);
|
||||||
|
|
||||||
const { appVersion, getLocalVersion} = useVersion();
|
const { appVersion, getLocalVersion } = useVersion();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appVersion) return;
|
if (appVersion) return;
|
||||||
|
|
@ -863,10 +859,9 @@ export default function KvmIdRoute() {
|
||||||
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
|
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
|
||||||
>
|
>
|
||||||
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
||||||
{!!ConnectionStatusElement && ConnectionStatusElement}
|
{isFailsafeMode && failsafeReason ? (
|
||||||
{isFailsafeMode && failsafeReason && (
|
|
||||||
<FailSafeModeOverlay reason={failsafeReason} />
|
<FailSafeModeOverlay reason={failsafeReason} />
|
||||||
)}
|
) : !!ConnectionStatusElement && ConnectionStatusElement}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SidebarContainer sidebarView={sidebarView} />
|
<SidebarContainer sidebarView={sidebarView} />
|
||||||
|
|
|
||||||
1
video.go
1
video.go
|
|
@ -27,6 +27,7 @@ func triggerVideoStateUpdate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetVideoState() (native.VideoState, error) {
|
func rpcGetVideoState() (native.VideoState, error) {
|
||||||
|
notifyFailsafeMode(currentSession)
|
||||||
return lastVideoState, nil
|
return lastVideoState, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,7 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
triggerVideoStateUpdate()
|
triggerVideoStateUpdate()
|
||||||
triggerUSBStateUpdate()
|
triggerUSBStateUpdate()
|
||||||
|
notifyFailsafeMode(session)
|
||||||
case "terminal":
|
case "terminal":
|
||||||
handleTerminalChannel(d)
|
handleTerminalChannel(d)
|
||||||
case "serial":
|
case "serial":
|
||||||
|
|
@ -391,10 +392,12 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func onActiveSessionsChanged() {
|
func onActiveSessionsChanged() {
|
||||||
|
notifyFailsafeMode(currentSession)
|
||||||
requestDisplayUpdate(true, "active_sessions_changed")
|
requestDisplayUpdate(true, "active_sessions_changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func onFirstSessionConnected() {
|
func onFirstSessionConnected() {
|
||||||
|
notifyFailsafeMode(currentSession)
|
||||||
_ = nativeInstance.VideoStart()
|
_ = nativeInstance.VideoStart()
|
||||||
stopVideoSleepModeTicker()
|
stopVideoSleepModeTicker()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue