Compare commits

...

7 Commits

16 changed files with 469 additions and 91 deletions

View File

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

107
failsafe.go Normal file
View File

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

View File

@ -1,5 +1,8 @@
//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
import (
@ -46,7 +49,17 @@ static inline void jetkvm_cgo_setup_rpc_handler() {
*/
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
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{}
func uiEventCodeToName(code int) string {
if cgoDisabled {
return ""
}
name, ok := eventCodeToNameMap[code]
if !ok {
cCode := C.int(code)
@ -103,6 +120,10 @@ func uiEventCodeToName(code int) string {
}
func setUpNativeHandlers() {
if cgoDisabled {
return
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -114,6 +135,10 @@ func setUpNativeHandlers() {
}
func uiInit(rotation uint16) {
if cgoDisabled {
return
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -123,6 +148,10 @@ func uiInit(rotation uint16) {
}
func uiTick() {
if cgoDisabled {
return
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -130,6 +159,10 @@ func uiTick() {
}
func videoInit(factor float64) error {
if cgoDisabled {
return nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -143,6 +176,10 @@ func videoInit(factor float64) error {
}
func videoShutdown() {
if cgoDisabled {
return
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -150,6 +187,10 @@ func videoShutdown() {
}
func videoStart() {
if cgoDisabled {
return
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -157,6 +198,10 @@ func videoStart() {
}
func videoStop() {
if cgoDisabled {
return
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -164,6 +209,10 @@ func videoStop() {
}
func videoLogStatus() string {
if cgoDisabled {
return ""
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -174,6 +223,10 @@ func videoLogStatus() string {
}
func uiSetVar(name string, value string) {
if cgoDisabled {
return
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -187,6 +240,10 @@ func uiSetVar(name string, value string) {
}
func uiGetVar(name string) string {
if cgoDisabled {
return ""
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -197,6 +254,10 @@ func uiGetVar(name string) string {
}
func uiSwitchToScreen(screen string) {
if cgoDisabled {
return
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -206,6 +267,10 @@ func uiSwitchToScreen(screen string) {
}
func uiGetCurrentScreen() string {
if cgoDisabled {
return ""
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -214,6 +279,10 @@ func uiGetCurrentScreen() string {
}
func uiObjAddState(objName string, state string) (bool, error) {
if cgoDisabled {
return false, nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -226,6 +295,10 @@ func uiObjAddState(objName string, state string) (bool, error) {
}
func uiObjClearState(objName string, state string) (bool, error) {
if cgoDisabled {
return false, nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -238,6 +311,10 @@ func uiObjClearState(objName string, state string) (bool, error) {
}
func uiGetLVGLVersion() string {
if cgoDisabled {
return ""
}
cgoLock.Lock()
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
func uiObjAddFlag(objName string, flag string) (bool, error) {
if cgoDisabled {
return false, nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -258,6 +339,10 @@ func uiObjAddFlag(objName string, flag string) (bool, error) {
}
func uiObjClearFlag(objName string, flag string) (bool, error) {
if cgoDisabled {
return false, nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -278,6 +363,10 @@ func uiObjShow(objName string) (bool, error) {
}
func uiObjSetOpacity(objName string, opacity int) (bool, error) {
if cgoDisabled {
return false, nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -289,6 +378,10 @@ func uiObjSetOpacity(objName string, opacity int) (bool, error) {
}
func uiObjFadeIn(objName string, duration uint32) (bool, error) {
if cgoDisabled {
return false, nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -301,6 +394,10 @@ func uiObjFadeIn(objName string, duration uint32) (bool, error) {
}
func uiObjFadeOut(objName string, duration uint32) (bool, error) {
if cgoDisabled {
return false, nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -313,6 +410,10 @@ func uiObjFadeOut(objName string, duration uint32) (bool, error) {
}
func uiLabelSetText(objName string, text string) (bool, error) {
if cgoDisabled {
return false, nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -330,6 +431,10 @@ func uiLabelSetText(objName string, text string) (bool, error) {
}
func uiImgSetSrc(objName string, src string) (bool, error) {
if cgoDisabled {
return false, nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -345,6 +450,10 @@ func uiImgSetSrc(objName string, src string) (bool, error) {
}
func uiDispSetRotation(rotation uint16) (bool, error) {
if cgoDisabled {
return false, nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -357,6 +466,10 @@ func uiDispSetRotation(rotation uint16) (bool, error) {
}
func videoGetStreamQualityFactor() (float64, error) {
if cgoDisabled {
return 0, nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -365,6 +478,10 @@ func videoGetStreamQualityFactor() (float64, error) {
}
func videoSetStreamQualityFactor(factor float64) error {
if cgoDisabled {
return nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -373,6 +490,10 @@ func videoSetStreamQualityFactor(factor float64) error {
}
func videoGetEDID() (string, error) {
if cgoDisabled {
return "", nil
}
cgoLock.Lock()
defer cgoLock.Unlock()
@ -381,6 +502,10 @@ func videoGetEDID() (string, error) {
}
func videoSetEDID(edid string) error {
if cgoDisabled {
return nil
}
cgoLock.Lock()
defer cgoLock.Unlock()

View File

@ -9,6 +9,7 @@ import (
)
type Native struct {
disable bool
ready chan struct{}
l *zerolog.Logger
lD *zerolog.Logger
@ -27,6 +28,7 @@ type Native struct {
}
type NativeOptions struct {
Disable bool
SystemVersion *semver.Version
AppVersion *semver.Version
DisplayRotation uint16
@ -74,6 +76,7 @@ func NewNative(opts NativeOptions) *Native {
}
return &Native{
disable: opts.Disable,
ready: make(chan struct{}),
l: nativeLogger,
lD: displayLogger,
@ -92,6 +95,12 @@ func NewNative(opts NativeOptions) *Native {
}
func (n *Native) Start() {
if n.disable {
nativeLogger.Warn().Msg("native is disabled, skipping initialization")
setCgoDisabled(true)
return
}
// set up singleton
setInstance(n)
setUpNativeHandlers()

View File

@ -1268,4 +1268,5 @@ var rpcHandlers = map[string]RPCHandler{
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
"getFailSafeLogs": {Func: rpcGetFailsafeLogs},
}

1
log.go
View File

@ -11,6 +11,7 @@ func ErrorfL(l *zerolog.Logger, format string, err error, args ...any) error {
var (
logger = logging.GetSubsystemLogger("jetkvm")
failsafeLogger = logging.GetSubsystemLogger("failsafe")
networkLogger = logging.GetSubsystemLogger("network")
cloudLogger = logging.GetSubsystemLogger("cloud")
websocketLogger = logging.GetSubsystemLogger("websocket")

View File

@ -14,6 +14,11 @@ import (
var appCtx context.Context
func Main() {
checkFailsafeReason()
if failsafeModeActive {
logger.Warn().Str("reason", failsafeModeReason).Msg("failsafe mode activated")
}
LoadConfig()
var cancel context.CancelFunc
@ -48,6 +53,7 @@ func Main() {
// Initialize network
if err := initNetwork(); err != nil {
logger.Error().Err(err).Msg("failed to initialize network")
// TODO: reset config to default
os.Exit(1)
}
@ -58,7 +64,6 @@ func Main() {
// Initialize mDNS
if err := initMdns(); err != nil {
logger.Error().Err(err).Msg("failed to initialize mDNS")
os.Exit(1)
}
initPrometheus()

View File

@ -17,6 +17,7 @@ var (
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeInstance = native.NewNative(native.NativeOptions{
Disable: failsafeModeActive,
SystemVersion: systemVersion,
AppVersion: appVersion,
DisplayRotation: config.GetDisplayRotation(),

View File

@ -1,9 +1,10 @@
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 { GridCard } from "@components/Card";
import Card, { GridCard } from "@components/Card";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import { useVersion } from "@/hooks/useVersion";
@ -30,19 +31,47 @@ function OverlayContent({ children }: OverlayContentProps) {
);
}
interface TooltipProps {
readonly children: React.ReactNode;
readonly text: string;
readonly show: boolean;
}
function Tooltip({ children, text, show }: TooltipProps) {
if (!show) {
return <>{children}</>;
}
return (
<div className="group/tooltip relative">
{children}
<div className="pointer-events-none absolute bottom-full left-1/2 mb-2 hidden -translate-x-1/2 opacity-0 transition-opacity group-hover/tooltip:block group-hover/tooltip:opacity-100">
<Card>
<div className="whitespace-nowrap px-2 py-1 text-xs flex items-center gap-1 justify-center">
<LuInfo className="h-3 w-3 text-slate-700 dark:text-slate-300" />
{text}
</div>
</Card>
</div>
</div>
);
}
export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) {
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. You can reboot to attempt recovery, report the issue, or downgrade to the last stable version.",
"We've detected an issue with the video capture process. Your device is still running and accessible, but video streaming is temporarily unavailable.",
};
default:
return {
@ -80,13 +109,14 @@ export function FailSafeModeOverlay({ reason }: FailSafeModeOverlayProps) {
document.body.removeChild(a);
URL.revokeObjectURL(url);
notifications.success("Recovery logs downloaded successfully");
notifications.success("Crash logs downloaded successfully");
setHasDownloadedLogs(true);
// Open GitHub issue
const issueBody = `## Issue Description
The ${reason} process encountered an error and recovery mode was activated.
The \`${reason}\` process encountered an error and fail safe mode was activated.
**Reason:** ${reason}
**Reason:** \`${reason}\`
**Timestamp:** ${new Date().toISOString()}
**App Version:** ${appVersion || "Unknown"}
**System Version:** ${systemVersion || "Unknown"}
@ -95,6 +125,9 @@ The ${reason} process encountered an error and recovery mode was activated.
Please attach the recovery logs file that was downloaded to your computer:
\`${filename}\`
> [!NOTE]
> Please omit any sensitive information from the logs.
## Additional Context
[Please describe what you were doing when this occurred]`;
@ -108,13 +141,13 @@ Please attach the recovery logs file that was downloaded to your computer:
};
const handleDowngrade = () => {
navigateTo("/settings/general/update?appVersion=0.4.8");
navigateTo("/settings/general/update?app=0.4.8");
};
return (
<AnimatePresence>
<motion.div
className="aspect-video h-full w-full"
className="aspect-video h-full w-full isolate"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0 } }}
@ -132,6 +165,7 @@ Please attach the recovery logs file that was downloaded to your computer:
<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}
@ -139,22 +173,32 @@ Please attach the recovery logs file that was downloaded to your computer:
size="SM"
disabled={isDownloadingLogs}
LeadingIcon={GitHubIcon}
text={isDownloadingLogs ? "Downloading Logs..." : "Report Issue & Download Logs"}
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>

View File

@ -930,12 +930,12 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
export interface FailsafeModeState {
isFailsafeMode: boolean;
reason: string | null; // "video", "network", etc.
setFailsafeMode: (enabled: boolean, reason: string | null) => void;
setFailsafeMode: (active: boolean, reason: string | null) => void;
}
export const useFailsafeModeStore = create<FailsafeModeState>(set => ({
isFailsafeMode: false,
reason: null,
setFailsafeMode: (enabled: boolean, reason: string | null) =>
set({ isFailsafeMode: enabled, reason }),
setFailsafeMode: (active: boolean, reason: string | null) =>
set({ isFailsafeMode: active, reason }),
}));

View File

@ -50,7 +50,7 @@ const blockedMethodsByReason: Record<string, string[]> = {
export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
const { rpcDataChannel } = useRTCStore();
const { isFailsafeMode: isFailsafeMode, reason } = useFailsafeModeStore();
const { isFailsafeMode, reason } = useFailsafeModeStore();
const send = useCallback(
async (method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => {

View File

@ -1,28 +1,42 @@
import { useNavigate } from "react-router";
import { useCallback } from "react";
import { useCallback, useState } from "react";
import { useJsonRpc } from "@/hooks/useJsonRpc";
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() {
const navigate = useNavigate();
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
send("reboot", {force: true});
}, [send]);
send("reboot", { force: true });
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. */
}
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
return <Dialog isRebooting={isRebooting} onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
}
export function Dialog({
isRebooting,
onClose,
onConfirmUpdate,
}: {
isRebooting: boolean;
onClose: () => void;
onConfirmUpdate: () => void;
}) {
@ -31,6 +45,7 @@ export function Dialog({
<div className="pointer-events-auto relative mx-auto text-left">
<div>
<ConfirmationBox
isRebooting={isRebooting}
onYes={onConfirmUpdate}
onNo={onClose}
/>
@ -40,9 +55,11 @@ export function Dialog({
}
function ConfirmationBox({
isRebooting,
onYes,
onNo,
}: {
isRebooting: boolean;
onYes: () => void;
onNo: () => void;
}) {
@ -55,11 +72,16 @@ function ConfirmationBox({
<p className="text-sm text-slate-600 dark:text-slate-300">
Do you want to proceed with rebooting the system?
</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">
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
<Button size="SM" theme="blank" text="No" onClick={onNo} />
</div>
)}
</div>
</div>
);

View File

@ -161,7 +161,7 @@ export default function SettingsRoute() {
</div>
</FeatureFlag>
<div className={cx("shrink-0", {
"opacity-50 cursor-not-allowed!": isVideoDisabled
"opacity-50 cursor-not-allowed": isVideoDisabled
})}>
<NavLink
to="video"

View File

@ -41,7 +41,7 @@ const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectio
const Terminal = lazy(() => import('@components/Terminal'));
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
import Modal from "@/components/Modal";
import { FailSafeModeOverlay } from "@components/FaileSafeModeOverlay";
import { FailSafeModeOverlay } from "@components/FailSafeModeOverlay";
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
import {
ConnectionFailedOverlay,
@ -126,7 +126,7 @@ export default function KvmIdRoute() {
const params = useParams() as { id: string };
const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore();
const [ queryParams, setQueryParams ] = useSearchParams();
const [queryParams, setQueryParams] = useSearchParams();
const {
peerConnection, setPeerConnection,
@ -483,10 +483,6 @@ export default function KvmIdRoute() {
const rpcDataChannel = pc.createDataChannel("rpc");
rpcDataChannel.onopen = () => {
setRpcDataChannel(rpcDataChannel);
// setTimeout(() => {
// useFailsafeModeStore.setState({ isFailsafeMode: true, reason: "video" });
// }, 1000);
};
const rpcHidChannel = pc.createDataChannel("hidrpc");
@ -604,7 +600,7 @@ export default function KvmIdRoute() {
});
}, 10000);
const { setNetworkState} = useNetworkStateStore();
const { setNetworkState } = useNetworkStateStore();
const { setHdmiState } = useVideoStore();
const {
keyboardLedState, setKeyboardLedState,
@ -676,9 +672,9 @@ export default function KvmIdRoute() {
}
if (resp.method === "failsafeMode") {
const { enabled, reason } = resp.params as { enabled: boolean; reason: string };
console.debug("Setting failsafe mode", { enabled, reason });
setFailsafeMode(enabled, reason);
const { active, reason } = resp.params as { active: boolean; reason: string };
console.debug("Setting failsafe mode", { active, reason });
setFailsafeMode(active, reason);
}
}
@ -770,7 +766,7 @@ export default function KvmIdRoute() {
if (location.pathname !== "/other-session") navigateTo("/");
}, [navigateTo, location.pathname]);
const { appVersion, getLocalVersion} = useVersion();
const { appVersion, getLocalVersion } = useVersion();
useEffect(() => {
if (appVersion) return;
@ -863,10 +859,9 @@ export default function KvmIdRoute() {
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center p-4"
>
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
{!!ConnectionStatusElement && ConnectionStatusElement}
{isFailsafeMode && failsafeReason && (
{isFailsafeMode && failsafeReason ? (
<FailSafeModeOverlay reason={failsafeReason} />
)}
) : !!ConnectionStatusElement && ConnectionStatusElement}
</div>
</div>
<SidebarContainer sidebarView={sidebarView} />

View File

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

View File

@ -289,6 +289,7 @@ func newSession(config SessionConfig) (*Session, error) {
triggerOTAStateUpdate()
triggerVideoStateUpdate()
triggerUSBStateUpdate()
notifyFailsafeMode(session)
case "terminal":
handleTerminalChannel(d)
case "serial":
@ -391,10 +392,12 @@ func newSession(config SessionConfig) (*Session, error) {
}
func onActiveSessionsChanged() {
notifyFailsafeMode(currentSession)
requestDisplayUpdate(true, "active_sessions_changed")
}
func onFirstSessionConnected() {
notifyFailsafeMode(currentSession)
_ = nativeInstance.VideoStart()
stopVideoSleepModeTicker()
}