Compare commits

..

No commits in common. "502cd4ecc279a1c720816e27946e8968f67aa075" and "6afb15b29a8db256f5c2954d57ad842eb79271e3" have entirely different histories.

16 changed files with 91 additions and 469 deletions

View File

@ -17,8 +17,8 @@ import (
const ( const (
envChildID = "JETKVM_CHILD_ID" envChildID = "JETKVM_CHILD_ID"
errorDumpDir = "/userdata/jetkvm/crashdump" errorDumpDir = "/userdata/jetkvm/"
errorDumpLastFile = "last-crash.log" errorDumpStateFile = ".has_error_dump"
errorDumpTemplate = "jetkvm-%s.log" errorDumpTemplate = "jetkvm-%s.log"
) )
@ -74,12 +74,7 @@ func supervise() error {
// run the child binary // run the child binary
cmd := exec.Command(binPath) cmd := exec.Command(binPath)
lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile) cmd.Env = append(os.Environ(), []string{envChildID + "=" + kvm.GetBuiltAppVersion()}...)
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")
@ -122,47 +117,30 @@ func supervise() error {
return nil return nil
} }
func isSymlinkTo(oldName, newName string) bool { func createErrorDump(logFile *os.File) {
file, err := os.Stat(newName) logFile.Close()
// touch the error dump state file
if err := os.WriteFile(filepath.Join(errorDumpDir, errorDumpStateFile), []byte{}, 0644); err != nil {
return
}
fileName := fmt.Sprintf(errorDumpTemplate, time.Now().Format("20060102150405"))
filePath := filepath.Join(errorDumpDir, fileName)
if err := os.Rename(logFile.Name(), filePath); err == nil {
fmt.Printf("error dump created: %s\n", filePath)
return
}
fnSrc, err := os.Open(logFile.Name())
if err != nil { if err != nil {
return false return
}
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(newName) fnDst, err := os.Create(filePath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create file: %w", err) return
} }
defer fnDst.Close() defer fnDst.Close()
@ -170,60 +148,18 @@ func renameFile(f *os.File, newName string) error {
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 fmt.Errorf("failed to read file: %w", err) return
} }
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 fmt.Errorf("failed to write file: %w", err)
}
}
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 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) fmt.Printf("error dump created: %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() {

View File

@ -1,107 +0,0 @@
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
}

View File

@ -1,8 +1,5 @@
//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 (
@ -49,17 +46,7 @@ static inline void jetkvm_cgo_setup_rpc_handler() {
*/ */
import "C" import "C"
var ( var cgoLock sync.Mutex
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) {
@ -104,10 +91,6 @@ 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)
@ -120,10 +103,6 @@ func uiEventCodeToName(code int) string {
} }
func setUpNativeHandlers() { func setUpNativeHandlers() {
if cgoDisabled {
return
}
cgoLock.Lock() cgoLock.Lock()
defer cgoLock.Unlock() defer cgoLock.Unlock()
@ -135,10 +114,6 @@ func setUpNativeHandlers() {
} }
func uiInit(rotation uint16) { func uiInit(rotation uint16) {
if cgoDisabled {
return
}
cgoLock.Lock() cgoLock.Lock()
defer cgoLock.Unlock() defer cgoLock.Unlock()
@ -148,10 +123,6 @@ func uiInit(rotation uint16) {
} }
func uiTick() { func uiTick() {
if cgoDisabled {
return
}
cgoLock.Lock() cgoLock.Lock()
defer cgoLock.Unlock() defer cgoLock.Unlock()
@ -159,10 +130,6 @@ 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()
@ -176,10 +143,6 @@ func videoInit(factor float64) error {
} }
func videoShutdown() { func videoShutdown() {
if cgoDisabled {
return
}
cgoLock.Lock() cgoLock.Lock()
defer cgoLock.Unlock() defer cgoLock.Unlock()
@ -187,10 +150,6 @@ func videoShutdown() {
} }
func videoStart() { func videoStart() {
if cgoDisabled {
return
}
cgoLock.Lock() cgoLock.Lock()
defer cgoLock.Unlock() defer cgoLock.Unlock()
@ -198,10 +157,6 @@ func videoStart() {
} }
func videoStop() { func videoStop() {
if cgoDisabled {
return
}
cgoLock.Lock() cgoLock.Lock()
defer cgoLock.Unlock() defer cgoLock.Unlock()
@ -209,10 +164,6 @@ func videoStop() {
} }
func videoLogStatus() string { func videoLogStatus() string {
if cgoDisabled {
return ""
}
cgoLock.Lock() cgoLock.Lock()
defer cgoLock.Unlock() defer cgoLock.Unlock()
@ -223,10 +174,6 @@ 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()
@ -240,10 +187,6 @@ 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()
@ -254,10 +197,6 @@ 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()
@ -267,10 +206,6 @@ func uiSwitchToScreen(screen string) {
} }
func uiGetCurrentScreen() string { func uiGetCurrentScreen() string {
if cgoDisabled {
return ""
}
cgoLock.Lock() cgoLock.Lock()
defer cgoLock.Unlock() defer cgoLock.Unlock()
@ -279,10 +214,6 @@ 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()
@ -295,10 +226,6 @@ 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()
@ -311,10 +238,6 @@ 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()
@ -323,10 +246,6 @@ 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()
@ -339,10 +258,6 @@ 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()
@ -363,10 +278,6 @@ 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()
@ -378,10 +289,6 @@ 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()
@ -394,10 +301,6 @@ 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()
@ -410,10 +313,6 @@ 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()
@ -431,10 +330,6 @@ 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()
@ -450,10 +345,6 @@ 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()
@ -466,10 +357,6 @@ 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()
@ -478,10 +365,6 @@ 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()
@ -490,10 +373,6 @@ 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()
@ -502,10 +381,6 @@ 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()

View File

@ -9,7 +9,6 @@ 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
@ -28,7 +27,6 @@ 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
@ -76,7 +74,6 @@ 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,
@ -95,12 +92,6 @@ 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()

View File

@ -1268,5 +1268,4 @@ 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
View File

@ -11,7 +11,6 @@ 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")

View File

@ -14,11 +14,6 @@ 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
@ -53,7 +48,6 @@ 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)
} }
@ -64,6 +58,7 @@ 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()

View File

@ -17,7 +17,6 @@ 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(),

View File

@ -1,10 +1,9 @@
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 Card, { GridCard } from "@components/Card"; import { 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";
@ -31,47 +30,19 @@ 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.", "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.",
}; };
default: default:
return { return {
@ -109,14 +80,13 @@ export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) {
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
notifications.success("Crash logs downloaded successfully"); notifications.success("Recovery 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 fail safe mode was activated. The ${reason} process encountered an error and recovery 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"}
@ -125,9 +95,6 @@ The \`${reason}\` process encountered an error and fail safe 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]`;
@ -141,13 +108,13 @@ Please attach the recovery logs file that was downloaded to your computer:
}; };
const handleDowngrade = () => { const handleDowngrade = () => {
navigateTo("/settings/general/update?app=0.4.8"); navigateTo("/settings/general/update?appVersion=0.4.8");
}; };
return ( return (
<AnimatePresence> <AnimatePresence>
<motion.div <motion.div
className="aspect-video h-full w-full isolate" className="aspect-video h-full w-full"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0 } }} exit={{ opacity: 0, transition: { duration: 0 } }}
@ -165,7 +132,6 @@ 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="space-y-3">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Button <Button
onClick={handleReportAndDownloadLogs} onClick={handleReportAndDownloadLogs}
@ -173,32 +139,22 @@ Please attach the recovery logs file that was downloaded to your computer:
size="SM" size="SM"
disabled={isDownloadingLogs} disabled={isDownloadingLogs}
LeadingIcon={GitHubIcon} LeadingIcon={GitHubIcon}
text={isDownloadingLogs ? "Downloading Logs..." : "Download Logs & Report Issue"} text={isDownloadingLogs ? "Downloading Logs..." : "Report Issue & Download Logs"}
/> />
<div className="h-8 w-px bg-slate-200 dark:bg-slate-700 block" />
<Tooltip text="Download logs first to unlock" show={!hasDownloadedLogs}>
<Button <Button
onClick={() => navigateTo("/settings/general/reboot")} onClick={() => navigateTo("/settings/general/reboot")}
theme="light" theme="light"
size="SM" size="SM"
text="Reboot Device" text="Reboot Device"
disabled={!hasDownloadedLogs}
/> />
</Tooltip>
<Tooltip text="Download logs first to unlock" show={!hasDownloadedLogs}>
<Button <Button
size="SM" size="SM"
onClick={handleDowngrade} onClick={handleDowngrade}
theme="light" theme="light"
text="Downgrade to v0.4.8" text="Downgrade to v0.4.8"
disabled={!hasDownloadedLogs}
/> />
</Tooltip>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -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: (active: boolean, reason: string | null) => void; setFailsafeMode: (enabled: 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: (active: boolean, reason: string | null) => setFailsafeMode: (enabled: boolean, reason: string | null) =>
set({ isFailsafeMode: active, reason }), set({ isFailsafeMode: enabled, reason }),
})); }));

View File

@ -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, reason } = useFailsafeModeStore(); const { isFailsafeMode: 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) => {

View File

@ -1,42 +1,28 @@
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { useCallback, useState } from "react"; import { useCallback } 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(async () => { const onConfirmUpdate = useCallback(() => {
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 isRebooting={isRebooting} onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />; return <Dialog 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;
}) { }) {
@ -45,7 +31,6 @@ export function Dialog({
<div className="pointer-events-auto relative mx-auto text-left"> <div className="pointer-events-auto relative mx-auto text-left">
<div> <div>
<ConfirmationBox <ConfirmationBox
isRebooting={isRebooting}
onYes={onConfirmUpdate} onYes={onConfirmUpdate}
onNo={onClose} onNo={onClose}
/> />
@ -55,11 +40,9 @@ export function Dialog({
} }
function ConfirmationBox({ function ConfirmationBox({
isRebooting,
onYes, onYes,
onNo, onNo,
}: { }: {
isRebooting: boolean;
onYes: () => void; onYes: () => void;
onNo: () => void; onNo: () => void;
}) { }) {
@ -72,16 +55,11 @@ 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 items-center justify-center">
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
</div>
) : (
<div className="mt-4 flex gap-x-2"> <div className="mt-4 flex gap-x-2">
<Button size="SM" theme="light" text="Yes" onClick={onYes} /> <Button size="SM" theme="light" text="Yes" onClick={onYes} />
<Button size="SM" theme="blank" text="No" onClick={onNo} /> <Button size="SM" theme="blank" text="No" onClick={onNo} />
</div> </div>
)}
</div> </div>
</div> </div>
); );

View File

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

View File

@ -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/FailSafeModeOverlay"; import { FailSafeModeOverlay } from "@components/FaileSafeModeOverlay";
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc"; import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
import { import {
ConnectionFailedOverlay, ConnectionFailedOverlay,
@ -483,6 +483,10 @@ 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");
@ -672,9 +676,9 @@ export default function KvmIdRoute() {
} }
if (resp.method === "failsafeMode") { if (resp.method === "failsafeMode") {
const { active, reason } = resp.params as { active: boolean; reason: string }; const { enabled, reason } = resp.params as { enabled: boolean; reason: string };
console.debug("Setting failsafe mode", { active, reason }); console.debug("Setting failsafe mode", { enabled, reason });
setFailsafeMode(active, reason); setFailsafeMode(enabled, reason);
} }
} }
@ -859,9 +863,10 @@ 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">
{isFailsafeMode && failsafeReason ? ( {!!ConnectionStatusElement && ConnectionStatusElement}
{isFailsafeMode && failsafeReason && (
<FailSafeModeOverlay reason={failsafeReason} /> <FailSafeModeOverlay reason={failsafeReason} />
) : !!ConnectionStatusElement && ConnectionStatusElement} )}
</div> </div>
</div> </div>
<SidebarContainer sidebarView={sidebarView} /> <SidebarContainer sidebarView={sidebarView} />

View File

@ -27,7 +27,6 @@ func triggerVideoStateUpdate() {
} }
func rpcGetVideoState() (native.VideoState, error) { func rpcGetVideoState() (native.VideoState, error) {
notifyFailsafeMode(currentSession)
return lastVideoState, nil return lastVideoState, nil
} }

View File

@ -289,7 +289,6 @@ 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":
@ -392,12 +391,10 @@ 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()
} }