mirror of https://github.com/jetkvm/kvm.git
Merge 502cd4ecc2 into 542f9c0862
This commit is contained in:
commit
9a40840244
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(),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { LuTriangleAlert } from "react-icons/lu";
|
||||||
|
|
||||||
|
import Card from "@components/Card";
|
||||||
|
|
||||||
|
interface FailsafeModeBannerProps {
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FailsafeModeBanner({ reason }: FailsafeModeBannerProps) {
|
||||||
|
const getReasonMessage = () => {
|
||||||
|
switch (reason) {
|
||||||
|
case "video":
|
||||||
|
return "Failsafe Mode Active: Video-related settings are currently unavailable";
|
||||||
|
default:
|
||||||
|
return "Failsafe Mode Active: Some settings may be unavailable";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<div className="diagonal-stripes flex items-center gap-3 p-4 rounded">
|
||||||
|
<LuTriangleAlert className="h-5 w-5 flex-shrink-0 text-red-600 dark:text-red-400" />
|
||||||
|
<p className="text-sm font-medium text-red-800 dark:text-white">
|
||||||
|
{getReasonMessage()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { LuInfo } from "react-icons/lu";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import Card, { GridCard } from "@components/Card";
|
||||||
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
|
import { useVersion } from "@/hooks/useVersion";
|
||||||
|
import { useDeviceStore } from "@/hooks/stores";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
|
import { GitHubIcon } from "./Icons";
|
||||||
|
|
||||||
|
interface FailSafeModeOverlayProps {
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OverlayContentProps {
|
||||||
|
readonly children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverlayContent({ children }: OverlayContentProps) {
|
||||||
|
return (
|
||||||
|
<GridCard cardClassName="h-full pointer-events-auto outline-hidden!">
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center rounded-md border border-slate-800/30 dark:border-slate-300/20">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const { send } = useJsonRpc();
|
||||||
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
const { appVersion } = useVersion();
|
||||||
|
const { systemVersion } = useDeviceStore();
|
||||||
|
const [isDownloadingLogs, setIsDownloadingLogs] = useState(false);
|
||||||
|
const [hasDownloadedLogs, setHasDownloadedLogs] = useState(false);
|
||||||
|
|
||||||
|
const getReasonCopy = () => {
|
||||||
|
switch (reason) {
|
||||||
|
case "video":
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
"We've detected an issue with the video capture process. Your device is still running and accessible, but video streaming is temporarily unavailable.",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
"A critical process has encountered an issue. Your device is still accessible, but some functionality may be temporarily unavailable.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { message } = getReasonCopy();
|
||||||
|
|
||||||
|
const handleReportAndDownloadLogs = () => {
|
||||||
|
setIsDownloadingLogs(true);
|
||||||
|
|
||||||
|
send("getFailSafeLogs", {}, (resp: JsonRpcResponse) => {
|
||||||
|
setIsDownloadingLogs(false);
|
||||||
|
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`Failed to get recovery logs: ${resp.error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download logs
|
||||||
|
const logContent = resp.result as string;
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
const filename = `jetkvm-recovery-${reason}-${timestamp}.txt`;
|
||||||
|
|
||||||
|
const blob = new Blob([logContent], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
notifications.success("Crash logs downloaded successfully");
|
||||||
|
setHasDownloadedLogs(true);
|
||||||
|
|
||||||
|
// Open GitHub issue
|
||||||
|
const issueBody = `## Issue Description
|
||||||
|
The \`${reason}\` process encountered an error and fail safe mode was activated.
|
||||||
|
|
||||||
|
**Reason:** \`${reason}\`
|
||||||
|
**Timestamp:** ${new Date().toISOString()}
|
||||||
|
**App Version:** ${appVersion || "Unknown"}
|
||||||
|
**System Version:** ${systemVersion || "Unknown"}
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
Please attach the recovery logs file that was downloaded to your computer:
|
||||||
|
\`${filename}\`
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Please omit any sensitive information from the logs.
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
[Please describe what you were doing when this occurred]`;
|
||||||
|
|
||||||
|
const issueUrl =
|
||||||
|
`https://github.com/jetkvm/kvm/issues/new?` +
|
||||||
|
`title=${encodeURIComponent(`Recovery Mode: ${reason} process issue`)}&` +
|
||||||
|
`body=${encodeURIComponent(issueBody)}`;
|
||||||
|
|
||||||
|
window.open(issueUrl, "_blank");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDowngrade = () => {
|
||||||
|
navigateTo("/settings/general/update?app=0.4.8");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
className="aspect-video h-full w-full isolate"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0, transition: { duration: 0 } }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.4,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OverlayContent>
|
||||||
|
<div className="flex max-w-lg flex-col items-start gap-y-1">
|
||||||
|
<ExclamationTriangleIcon className="h-12 w-12 text-yellow-500" />
|
||||||
|
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2 text-black dark:text-white">
|
||||||
|
<h2 className="text-xl font-bold">Fail safe mode activated</h2>
|
||||||
|
<p className="text-sm">{message}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleReportAndDownloadLogs}
|
||||||
|
theme="primary"
|
||||||
|
size="SM"
|
||||||
|
disabled={isDownloadingLogs}
|
||||||
|
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>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</OverlayContent>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -465,7 +465,7 @@ export interface KeysDownState {
|
||||||
keys: number[];
|
keys: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type USBStates =
|
export type USBStates =
|
||||||
| "configured"
|
| "configured"
|
||||||
| "attached"
|
| "attached"
|
||||||
| "not attached"
|
| "not attached"
|
||||||
|
|
@ -926,3 +926,16 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export interface FailsafeModeState {
|
||||||
|
isFailsafeMode: boolean;
|
||||||
|
reason: string | null; // "video", "network", etc.
|
||||||
|
setFailsafeMode: (active: boolean, reason: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFailsafeModeStore = create<FailsafeModeState>(set => ({
|
||||||
|
isFailsafeMode: false,
|
||||||
|
reason: null,
|
||||||
|
setFailsafeMode: (active: boolean, reason: string | null) =>
|
||||||
|
set({ isFailsafeMode: active, reason }),
|
||||||
|
}));
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { useRTCStore } from "@/hooks/stores";
|
import { useRTCStore, useFailsafeModeStore } from "@/hooks/stores";
|
||||||
|
|
||||||
export interface JsonRpcRequest {
|
export interface JsonRpcRequest {
|
||||||
jsonrpc: string;
|
jsonrpc: string;
|
||||||
|
|
@ -34,12 +34,51 @@ export const RpcMethodNotFound = -32601;
|
||||||
const callbackStore = new Map<number | string, (resp: JsonRpcResponse) => void>();
|
const callbackStore = new Map<number | string, (resp: JsonRpcResponse) => void>();
|
||||||
let requestCounter = 0;
|
let requestCounter = 0;
|
||||||
|
|
||||||
|
// Map of blocked RPC methods by failsafe reason
|
||||||
|
const blockedMethodsByReason: Record<string, string[]> = {
|
||||||
|
video: [
|
||||||
|
'setStreamQualityFactor',
|
||||||
|
'getEDID',
|
||||||
|
'setEDID',
|
||||||
|
'getVideoLogStatus',
|
||||||
|
'setDisplayRotation',
|
||||||
|
'getVideoSleepMode',
|
||||||
|
'setVideoSleepMode',
|
||||||
|
'getVideoState',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
||||||
const { rpcDataChannel } = useRTCStore();
|
const { rpcDataChannel } = useRTCStore();
|
||||||
|
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) => {
|
||||||
if (rpcDataChannel?.readyState !== "open") return;
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
|
|
||||||
|
// Check if method is blocked in failsafe mode
|
||||||
|
if (isFailsafeMode && reason) {
|
||||||
|
const blockedMethods = blockedMethodsByReason[reason] || [];
|
||||||
|
if (blockedMethods.includes(method)) {
|
||||||
|
console.warn(`RPC method "${method}" is blocked in failsafe mode (reason: ${reason})`);
|
||||||
|
|
||||||
|
// Call callback with error if provided
|
||||||
|
if (callback) {
|
||||||
|
const errorResponse: JsonRpcErrorResponse = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
error: {
|
||||||
|
code: -32000,
|
||||||
|
message: "Method unavailable in failsafe mode",
|
||||||
|
data: `This feature is unavailable while in failsafe mode (${reason})`,
|
||||||
|
},
|
||||||
|
id: requestCounter + 1,
|
||||||
|
};
|
||||||
|
callback(errorResponse);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
requestCounter++;
|
requestCounter++;
|
||||||
const payload = { jsonrpc: "2.0", method, params, id: requestCounter };
|
const payload = { jsonrpc: "2.0", method, params, id: requestCounter };
|
||||||
// Store the callback if it exists
|
// Store the callback if it exists
|
||||||
|
|
@ -47,7 +86,7 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
|
||||||
|
|
||||||
rpcDataChannel.send(JSON.stringify(payload));
|
rpcDataChannel.send(JSON.stringify(payload));
|
||||||
},
|
},
|
||||||
[rpcDataChannel]
|
[rpcDataChannel, isFailsafeMode, reason]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -354,3 +354,11 @@ video::-webkit-media-controls {
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diagonal-stripes {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 0, 0, 0.1) 0 12px, /* red-50 with 20% opacity */
|
||||||
|
transparent 12px 24px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ import { cx } from "@/cva.config";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import { LinkButton } from "@components/Button";
|
import { LinkButton } from "@components/Button";
|
||||||
import { FeatureFlag } from "@components/FeatureFlag";
|
import { FeatureFlag } from "@components/FeatureFlag";
|
||||||
import { useUiStore } from "@/hooks/stores";
|
import { useUiStore, useFailsafeModeStore } from "@/hooks/stores";
|
||||||
|
import { FailsafeModeBanner } from "@components/FailSafeModeBanner";
|
||||||
|
|
||||||
/* 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. */
|
||||||
export default function SettingsRoute() {
|
export default function SettingsRoute() {
|
||||||
|
|
@ -29,6 +30,8 @@ export default function SettingsRoute() {
|
||||||
const [showLeftGradient, setShowLeftGradient] = useState(false);
|
const [showLeftGradient, setShowLeftGradient] = useState(false);
|
||||||
const [showRightGradient, setShowRightGradient] = useState(false);
|
const [showRightGradient, setShowRightGradient] = useState(false);
|
||||||
const { width = 0 } = useResizeObserver({ ref: scrollContainerRef as React.RefObject<HTMLDivElement> });
|
const { width = 0 } = useResizeObserver({ ref: scrollContainerRef as React.RefObject<HTMLDivElement> });
|
||||||
|
const { isFailsafeMode: isFailsafeMode, reason: failsafeReason } = useFailsafeModeStore();
|
||||||
|
const isVideoDisabled = isFailsafeMode && failsafeReason === "video";
|
||||||
|
|
||||||
// Handle scroll position to show/hide gradients
|
// Handle scroll position to show/hide gradients
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
|
|
@ -157,21 +160,28 @@ export default function SettingsRoute() {
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</FeatureFlag>
|
</FeatureFlag>
|
||||||
<div className="shrink-0">
|
<div className={cx("shrink-0", {
|
||||||
|
"opacity-50 cursor-not-allowed": isVideoDisabled
|
||||||
|
})}>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="video"
|
to="video"
|
||||||
className={({ isActive }) => (isActive ? "active" : "")}
|
className={({ isActive }) => cx(isActive ? "active" : "", {
|
||||||
>
|
"pointer-events-none": isVideoDisabled
|
||||||
|
})} >
|
||||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||||
<LuVideo className="h-4 w-4 shrink-0" />
|
<LuVideo className="h-4 w-4 shrink-0" />
|
||||||
<h1>Video</h1>
|
<h1>Video</h1>
|
||||||
</div>
|
</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">
|
<div className={cx("shrink-0", {
|
||||||
|
"opacity-50 cursor-not-allowed": isVideoDisabled
|
||||||
|
})}>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="hardware"
|
to="hardware"
|
||||||
className={({ isActive }) => (isActive ? "active" : "")}
|
className={({ isActive }) => cx(isActive ? "active" : "", {
|
||||||
|
"pointer-events-none": isVideoDisabled
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||||
<LuCpu className="h-4 w-4 shrink-0" />
|
<LuCpu className="h-4 w-4 shrink-0" />
|
||||||
|
|
@ -237,8 +247,8 @@ export default function SettingsRoute() {
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full md:col-span-6">
|
<div className="w-full md:col-span-6 space-y-4">
|
||||||
{/* <AutoHeight> */}
|
{isFailsafeMode && failsafeReason && <FailsafeModeBanner reason={failsafeReason} />}
|
||||||
<Card className="dark:bg-slate-800">
|
<Card className="dark:bg-slate-800">
|
||||||
<div
|
<div
|
||||||
className="space-y-4 px-8 py-6"
|
className="space-y-4 px-8 py-6"
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import {
|
||||||
useUpdateStore,
|
useUpdateStore,
|
||||||
useVideoStore,
|
useVideoStore,
|
||||||
VideoState,
|
VideoState,
|
||||||
|
useFailsafeModeStore,
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import WebRTCVideo from "@components/WebRTCVideo";
|
import WebRTCVideo from "@components/WebRTCVideo";
|
||||||
import DashboardNavbar from "@components/Header";
|
import DashboardNavbar from "@components/Header";
|
||||||
|
|
@ -40,6 +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/FailSafeModeOverlay";
|
||||||
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import {
|
import {
|
||||||
ConnectionFailedOverlay,
|
ConnectionFailedOverlay,
|
||||||
|
|
@ -113,6 +115,7 @@ const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
|
||||||
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params);
|
return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default function KvmIdRoute() {
|
export default function KvmIdRoute() {
|
||||||
const loaderResp = useLoaderData() as LocalLoaderResp | CloudLoaderResp;
|
const loaderResp = useLoaderData() as LocalLoaderResp | CloudLoaderResp;
|
||||||
// Depending on the mode, we set the appropriate variables
|
// Depending on the mode, we set the appropriate variables
|
||||||
|
|
@ -123,9 +126,9 @@ 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,
|
||||||
peerConnectionState, setPeerConnectionState,
|
peerConnectionState, setPeerConnectionState,
|
||||||
setMediaStream,
|
setMediaStream,
|
||||||
|
|
@ -597,13 +600,14 @@ 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);
|
||||||
|
const { setFailsafeMode } = useFailsafeModeStore();
|
||||||
|
|
||||||
const [hasUpdated, setHasUpdated] = useState(false);
|
const [hasUpdated, setHasUpdated] = useState(false);
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
|
@ -666,6 +670,12 @@ export default function KvmIdRoute() {
|
||||||
window.location.href = currentUrl.toString();
|
window.location.href = currentUrl.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resp.method === "failsafeMode") {
|
||||||
|
const { active, reason } = resp.params as { active: boolean; reason: string };
|
||||||
|
console.debug("Setting failsafe mode", { active, reason });
|
||||||
|
setFailsafeMode(active, reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { send } = useJsonRpc(onJsonRpcRequest);
|
const { send } = useJsonRpc(onJsonRpcRequest);
|
||||||
|
|
@ -756,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;
|
||||||
|
|
@ -764,6 +774,8 @@ export default function KvmIdRoute() {
|
||||||
getLocalVersion();
|
getLocalVersion();
|
||||||
}, [appVersion, getLocalVersion]);
|
}, [appVersion, getLocalVersion]);
|
||||||
|
|
||||||
|
const { isFailsafeMode, reason: failsafeReason } = useFailsafeModeStore();
|
||||||
|
|
||||||
const ConnectionStatusElement = useMemo(() => {
|
const ConnectionStatusElement = useMemo(() => {
|
||||||
const hasConnectionFailed =
|
const hasConnectionFailed =
|
||||||
connectionFailed || ["failed", "closed"].includes(peerConnectionState ?? "");
|
connectionFailed || ["failed", "closed"].includes(peerConnectionState ?? "");
|
||||||
|
|
@ -841,13 +853,15 @@ export default function KvmIdRoute() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative flex h-full w-full overflow-hidden">
|
<div className="relative flex h-full w-full overflow-hidden">
|
||||||
<WebRTCVideo />
|
{!isFailsafeMode && <WebRTCVideo />}
|
||||||
<div
|
<div
|
||||||
style={{ animationDuration: "500ms" }}
|
style={{ animationDuration: "500ms" }}
|
||||||
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 ? (
|
||||||
|
<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