Compare commits

..

No commits in common. "63c2272c4589c9b052ee75d5496a555af1a1632b" and "77b4c1c531b54e3c9466ff12191ff28c046467df" have entirely different histories.

21 changed files with 1311 additions and 1799 deletions

View File

@ -137,29 +137,6 @@ func (u *UsbGadget) GetPath(itemKey string) (string, error) {
return joinPath(u.kvmGadgetPath, item.path), nil return joinPath(u.kvmGadgetPath, item.path), nil
} }
// OverrideGadgetConfig overrides the gadget config for the given item and attribute.
// It returns an error if the item is not found or the attribute is not found.
// It returns true if the attribute is overridden, false otherwise.
func (u *UsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool) {
u.configLock.Lock()
defer u.configLock.Unlock()
// get it as a pointer
_, ok := u.configMap[itemKey]
if !ok {
return fmt.Errorf("config item %s not found", itemKey), false
}
if u.configMap[itemKey].attrs[itemAttr] == value {
return nil, false
}
u.configMap[itemKey].attrs[itemAttr] = value
u.log.Info().Str("itemKey", itemKey).Str("itemAttr", itemAttr).Str("value", value).Msg("overriding gadget config")
return nil, true
}
func mountConfigFS() error { func mountConfigFS() error {
_, err := os.Stat(gadgetPath) _, err := os.Stat(gadgetPath)
// TODO: check if it's mounted properly // TODO: check if it's mounted properly

View File

@ -14,13 +14,10 @@ var massStorageLun0Config = gadgetConfigItem{
order: 3001, order: 3001,
path: []string{"functions", "mass_storage.usb0", "lun.0"}, path: []string{"functions", "mass_storage.usb0", "lun.0"},
attrs: gadgetAttributes{ attrs: gadgetAttributes{
"cdrom": "1", "cdrom": "1",
"ro": "1", "ro": "1",
"removable": "1", "removable": "1",
"file": "\n", "file": "\n",
// the additional whitespace is intentional to avoid the "JetKVM V irtual Media" string "inquiry_string": "JetKVM Virtual Media",
// https://github.com/jetkvm/rv1106-system/blob/778133a1c153041e73f7de86c9c434a2753ea65d/sysdrv/source/uboot/u-boot/drivers/usb/gadget/f_mass_storage.c#L2556
// Vendor (8 chars), product (16 chars)
"inquiry_string": "JetKVM Virtual Media",
}, },
} }

View File

@ -566,12 +566,9 @@ type RPCHandler struct {
func rpcSetMassStorageMode(mode string) (string, error) { func rpcSetMassStorageMode(mode string) (string, error) {
logger.Info().Str("mode", mode).Msg("Setting mass storage mode") logger.Info().Str("mode", mode).Msg("Setting mass storage mode")
var cdrom bool var cdrom bool
switch mode { if mode == "cdrom" {
case "cdrom":
cdrom = true cdrom = true
case "file": } else if mode != "file" {
cdrom = false
default:
logger.Info().Str("mode", mode).Msg("Invalid mode provided") logger.Info().Str("mode", mode).Msg("Invalid mode provided")
return "", fmt.Errorf("invalid mode: %s", mode) return "", fmt.Errorf("invalid mode: %s", mode)
} }
@ -590,7 +587,7 @@ func rpcSetMassStorageMode(mode string) (string, error) {
} }
func rpcGetMassStorageMode() (string, error) { func rpcGetMassStorageMode() (string, error) {
cdrom, err := getMassStorageCDROMEnabled() cdrom, err := getMassStorageMode()
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get mass storage mode: %w", err) return "", fmt.Errorf("failed to get mass storage mode: %w", err)
} }

View File

@ -77,11 +77,6 @@ func Main() {
initUsbGadget() initUsbGadget()
err = setInitialVirtualMediaState()
if err != nil {
logger.Warn().Err(err).Msg("failed to set initial virtual media state")
}
go func() { go func() {
time.Sleep(15 * time.Minute) time.Sleep(15 * time.Minute)
for { for {

2627
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,8 +19,8 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.2", "@headlessui/react": "^2.2.0",
"@headlessui/tailwindcss": "^0.2.2", "@headlessui/tailwindcss": "^0.2.1",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@vitejs/plugin-basic-ssl": "^1.2.0", "@vitejs/plugin-basic-ssl": "^1.2.0",
"@xterm/addon-clipboard": "^0.1.0", "@xterm/addon-clipboard": "^0.1.0",
@ -36,43 +36,44 @@
"framer-motion": "^11.15.0", "framer-motion": "^11.15.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"mini-svg-data-uri": "^1.4.4", "mini-svg-data-uri": "^1.4.4",
"react": "^19.1.0", "react": "^18.2.0",
"react-animate-height": "^3.2.3", "react-animate-height": "^3.2.3",
"react-dom": "^19.1.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.4.1",
"react-icons": "^5.5.0", "react-icons": "^5.4.0",
"react-router-dom": "^6.22.3", "react-router-dom": "^6.22.3",
"react-simple-keyboard": "^3.8.71", "react-simple-keyboard": "^3.7.112",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.9",
"recharts": "^2.15.3", "recharts": "^2.15.0",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.0",
"validator": "^13.15.0", "validator": "^13.12.0",
"xterm": "^5.3.0",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.15",
"@types/react": "^19.1.3", "@types/react": "^18.2.66",
"@types/react-dom": "^19.1.3", "@types/react-dom": "^18.3.0",
"@types/semver": "^7.7.0", "@types/semver": "^7.5.8",
"@types/validator": "^13.15.0", "@types/validator": "^13.12.2",
"@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.32.0", "@typescript-eslint/parser": "^8.25.0",
"@vitejs/plugin-react-swc": "^3.9.0", "@vitejs/plugin-react-swc": "^3.7.2",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.20",
"eslint": "^9.26.0", "eslint": "^8.20.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.19",
"postcss": "^8.5.3", "postcss": "^8.4.49",
"prettier": "^3.5.3", "prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.8.3", "typescript": "^5.7.2",
"vite": "^5.2.0", "vite": "^5.2.0",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
} }

View File

@ -1,4 +1,4 @@
import React, { JSX } from "react"; import React from "react";
import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom"; import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
import ExtLink from "@/components/ExtLink"; import ExtLink from "@/components/ExtLink";

View File

@ -1,5 +1,5 @@
import type { Ref } from "react"; import type { Ref } from "react";
import React, { forwardRef, JSX } from "react"; import React, { forwardRef } from "react";
import clsx from "clsx"; import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel"; import FieldLabel from "@/components/FieldLabel";

View File

@ -1,5 +1,5 @@
import type { Ref } from "react"; import type { Ref } from "react";
import React, { forwardRef, JSX } from "react"; import React, { forwardRef } from "react";
import clsx from "clsx"; import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel"; import FieldLabel from "@/components/FieldLabel";

View File

@ -1,4 +1,4 @@
import React, { JSX } from "react"; import React from "react";
import clsx from "clsx"; import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel"; import FieldLabel from "@/components/FieldLabel";

View File

@ -79,11 +79,10 @@ function Terminal({
return () => { return () => {
setDisableKeyboardFocusTrap(false); setDisableKeyboardFocusTrap(false);
}; };
}, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]); }, [enableTerminal, instance, ref, setDisableKeyboardFocusTrap, type]);
const readyState = dataChannel.readyState; const readyState = dataChannel.readyState;
useEffect(() => { useEffect(() => {
if (!instance) return;
if (readyState !== "open") return; if (readyState !== "open") return;
const abortController = new AbortController(); const abortController = new AbortController();
@ -94,10 +93,11 @@ function Terminal({
// Handle binary data differently based on browser implementation // Handle binary data differently based on browser implementation
// Firefox sends data as blobs, chrome sends data as arraybuffer // Firefox sends data as blobs, chrome sends data as arraybuffer
if (binaryType === "arraybuffer") { if (binaryType === "arraybuffer") {
instance.write(new Uint8Array(e.data)); instance?.write(new Uint8Array(e.data));
} else if (binaryType === "blob") { } else if (binaryType === "blob") {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
if (!instance) return;
if (!reader.result) return; if (!reader.result) return;
instance.write(new Uint8Array(reader.result as ArrayBuffer)); instance.write(new Uint8Array(reader.result as ArrayBuffer));
}; };
@ -107,12 +107,12 @@ function Terminal({
{ signal: abortController.signal }, { signal: abortController.signal },
); );
const onDataHandler = instance.onData(data => { const onDataHandler = instance?.onData(data => {
dataChannel.send(data); dataChannel.send(data);
}); });
// Setup escape key handler // Setup escape key handler
const onKeyHandler = instance.onKey(e => { const onKeyHandler = instance?.onKey(e => {
const { domEvent } = e; const { domEvent } = e;
if (domEvent.key === "Escape") { if (domEvent.key === "Escape") {
setTerminalType("none"); setTerminalType("none");
@ -123,32 +123,32 @@ function Terminal({
// Send initial terminal size // Send initial terminal size
if (dataChannel.readyState === "open") { if (dataChannel.readyState === "open") {
dataChannel.send(JSON.stringify({ rows: instance.rows, cols: instance.cols })); dataChannel.send(JSON.stringify({ rows: instance?.rows, cols: instance?.cols }));
} }
return () => { return () => {
abortController.abort(); abortController.abort();
onDataHandler.dispose(); onDataHandler?.dispose();
onKeyHandler.dispose(); onKeyHandler?.dispose();
}; };
}, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]); }, [dataChannel, instance, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
useEffect(() => { useEffect(() => {
if (!instance) return; if (!instance) return;
// Load the fit addon // Load the fit addon
const fitAddon = new FitAddon(); const fitAddon = new FitAddon();
instance.loadAddon(fitAddon); instance?.loadAddon(fitAddon);
instance.loadAddon(new ClipboardAddon()); instance?.loadAddon(new ClipboardAddon());
instance.loadAddon(new Unicode11Addon()); instance?.loadAddon(new Unicode11Addon());
instance.loadAddon(new WebLinksAddon()); instance?.loadAddon(new WebLinksAddon());
instance.unicode.activeVersion = "11"; instance.unicode.activeVersion = "11";
if (isWebGl2Supported) { if (isWebGl2Supported) {
const webGl2Addon = new WebglAddon(); const webGl2Addon = new WebglAddon();
webGl2Addon.onContextLoss(() => webGl2Addon.dispose()); webGl2Addon.onContextLoss(() => webGl2Addon.dispose());
instance.loadAddon(webGl2Addon); instance?.loadAddon(webGl2Addon);
} }
const handleResize = () => fitAddon.fit(); const handleResize = () => fitAddon.fit();
@ -158,11 +158,13 @@ function Terminal({
return () => { return () => {
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
}; };
}, [ref, instance]); }, [ref, instance, dataChannel]);
return ( return (
<div <div
onKeyDown={e => e.stopPropagation()} onKeyDown={e => {
e.stopPropagation();
}}
onKeyUp={e => e.stopPropagation()} onKeyUp={e => e.stopPropagation()}
> >
<div> <div>

View File

@ -1,4 +1,4 @@
import React, { JSX } from "react"; import React from "react";
import clsx from "clsx"; import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel"; import FieldLabel from "@/components/FieldLabel";

View File

@ -10,7 +10,7 @@ import {
useVideoStore, useVideoStore,
} from "@/hooks/stores"; } from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings"; import { keys, modifiers } from "@/keyboardMappings";
import { useResizeObserver } from "usehooks-ts"; import { useResizeObserver } from "@/hooks/useResizeObserver";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import VirtualKeyboard from "@components/VirtualKeyboard"; import VirtualKeyboard from "@components/VirtualKeyboard";
import Actionbar from "@components/ActionBar"; import Actionbar from "@components/ActionBar";
@ -67,7 +67,7 @@ export default function WebRTCVideo() {
// Video-related // Video-related
useResizeObserver({ useResizeObserver({
ref: videoElm as React.RefObject<HTMLElement>, ref: videoElm,
onResize: ({ width, height }) => { onResize: ({ width, height }) => {
// This is actually client size, not videoSize // This is actually client size, not videoSize
if (width && height) { if (width && height) {
@ -330,31 +330,11 @@ export default function WebRTCVideo() {
) )
// Alt: Keep if Alt is pressed or if the key isn't an Alt key // Alt: Keep if Alt is pressed or if the key isn't an Alt key
// Example: If altKey is true, keep all modifiers // Example: If altKey is true, keep all modifiers
// If altKey is false, filter out 0x04 (AltLeft) // If altKey is false, filter out 0x04 (AltLeft) and 0x40 (AltRight)
//
// But intentionally do not filter out 0x40 (AltRight) to accomodate
// Alt Gr (Alt Graph) as a modifier. Oddly, Alt Gr does not declare
// itself to be an altKey. For example, the KeyboardEvent for
// Alt Gr + 2 has the following structure:
// - altKey: false
// - code: "Digit2"
// - type: [ "keydown" | "keyup" ]
//
// For context, filteredModifiers aims to keep track which modifiers
// are being pressed on the physical keyboard at any point in time.
// There is logic in the keyUpHandler and keyDownHandler to add and
// remove 0x40 (AltRight) from the list of new modifiers.
//
// But relying on the two handlers alone to track the state of the
// modifier bears the risk that the key up event for Alt Gr could
// get lost while the browser window is temporarily out of focus,
// which means the Alt Gr key state would then be "stuck". At this
// point, we would need to rely on the user to press Alt Gr again
// to properly release the state of that modifier.
.filter( .filter(
modifier => modifier =>
altKey || altKey ||
(modifier !== modifiers["AltLeft"]), (modifier !== modifiers["AltLeft"] && modifier !== modifiers["AltRight"]),
) )
// Meta: Keep if Meta is pressed or if the key isn't a Meta key // Meta: Keep if Meta is pressed or if the key isn't a Meta key
// Example: If metaKey is true, keep all modifiers // Example: If metaKey is true, keep all modifiers

View File

@ -0,0 +1,21 @@
import { useEffect, useRef } from "react";
export default function useInterval(callback: () => void, delay: number) {
const savedCallback = useRef<typeof callback>();
// Save the callback directly in the useRef object
savedCallback.current = callback;
// Set up the interval.
useEffect(() => {
function tick() {
if (!savedCallback.current) return;
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}

View File

@ -0,0 +1,26 @@
import { useCallback, useEffect, useRef } from "react";
/**
* Custom hook that determines if the component is currently mounted.
* @returns {() => boolean} A function that returns a boolean value indicating whether the component is mounted.
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-is-mounted)
* @example
* ```tsx
* const isComponentMounted = useIsMounted();
* // Use isComponentMounted() to check if the component is currently mounted before performing certain actions.
* ```
*/
export function useIsMounted(): () => boolean {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return useCallback(() => isMounted.current, []);
}

View File

@ -0,0 +1,131 @@
import { useEffect, useRef, useState } from "react";
import type { RefObject } from "react";
import { useIsMounted } from "./useIsMounted";
/** The size of the observed element. */
interface Size {
/** The width of the observed element. */
width: number | undefined;
/** The height of the observed element. */
height: number | undefined;
}
/** The options for the ResizeObserver. */
interface UseResizeObserverOptions<T extends HTMLElement = HTMLElement> {
/** The ref of the element to observe. */
ref: RefObject<T>;
/**
* When using `onResize`, the hook doesn't re-render on element size changes; it delegates handling to the provided callback.
* @default undefined
*/
onResize?: (size: Size) => void;
/**
* The box model to use for the ResizeObserver.
* @default 'content-box'
*/
box?: "border-box" | "content-box" | "device-pixel-content-box";
}
const initialSize: Size = {
width: undefined,
height: undefined,
};
/**
* Custom hook that observes the size of an element using the ResizeObserver API.
* @template T - The type of the element to observe.
* @param {UseResizeObserverOptions<T>} options - The options for the ResizeObserver.
* @returns {Size} - The size of the observed element.
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-resize-observer)
* @see [MDN ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* @example
* ```tsx
* const myRef = useRef(null);
* const { width = 0, height = 0 } = useResizeObserver({
* ref: myRef,
* box: 'content-box',
* });
*
* <div ref={myRef}>Hello, world!</div>
* ```
*/
export function useResizeObserver<T extends HTMLElement = HTMLElement>(
options: UseResizeObserverOptions<T>,
): Size {
const { ref, box = "content-box" } = options;
const [{ width, height }, setSize] = useState<Size>(initialSize);
const isMounted = useIsMounted();
const previousSize = useRef<Size>({ ...initialSize });
const onResize = useRef<((size: Size) => void) | undefined>(undefined);
onResize.current = options.onResize;
useEffect(() => {
if (!ref.current) return;
if (typeof window === "undefined" || !("ResizeObserver" in window)) return;
const observer = new ResizeObserver(([entry]) => {
const boxProp =
box === "border-box"
? "borderBoxSize"
: box === "device-pixel-content-box"
? "devicePixelContentBoxSize"
: "contentBoxSize";
const newWidth = extractSize(entry, boxProp, "inlineSize");
const newHeight = extractSize(entry, boxProp, "blockSize");
const hasChanged =
previousSize.current.width !== newWidth ||
previousSize.current.height !== newHeight;
if (hasChanged) {
const newSize: Size = { width: newWidth, height: newHeight };
previousSize.current.width = newWidth;
previousSize.current.height = newHeight;
if (onResize.current) {
onResize.current(newSize);
} else {
if (isMounted()) {
setSize(newSize);
}
}
}
});
observer.observe(ref.current, { box });
return () => {
observer.disconnect();
};
}, [box, isMounted, ref]);
return { width, height };
}
/** @private */
type BoxSizesKey = keyof Pick<
ResizeObserverEntry,
"borderBoxSize" | "contentBoxSize" | "devicePixelContentBoxSize"
>;
function extractSize(
entry: ResizeObserverEntry,
box: BoxSizesKey,
sizeType: keyof ResizeObserverSize,
): number | undefined {
if (!entry[box]) {
if (box === "contentBoxSize") {
return entry.contentRect[sizeType === "inlineSize" ? "width" : "height"];
}
return undefined;
}
return Array.isArray(entry[box])
? entry[box][0][sizeType]
: // @ts-expect-error Support Firefox's non-standard behavior
(entry[box][sizeType] as number);
}

View File

@ -1,6 +1,6 @@
// Key codes and modifiers correspond to definitions in the
// [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt)
export const keys = { export const keys = {
AltLeft: 0xe2,
AltRight: 0xe6,
ArrowDown: 0x51, ArrowDown: 0x51,
ArrowLeft: 0x50, ArrowLeft: 0x50,
ArrowRight: 0x4f, ArrowRight: 0x4f,

View File

@ -414,7 +414,7 @@ function BrowserFileView({
if (file?.name.endsWith(".iso")) { if (file?.name.endsWith(".iso")) {
setUsbMode("CDROM"); setUsbMode("CDROM");
} else if (file?.name.endsWith(".img")) { } else if (file?.name.endsWith(".img")) {
setUsbMode("Disk"); setUsbMode("CDROM");
} }
}; };
@ -566,7 +566,7 @@ function UrlView({
if (url.endsWith(".iso")) { if (url.endsWith(".iso")) {
setUsbMode("CDROM"); setUsbMode("CDROM");
} else if (url.endsWith(".img")) { } else if (url.endsWith(".img")) {
setUsbMode("Disk"); setUsbMode("CDROM");
} }
} }
@ -773,7 +773,7 @@ function DeviceFileView({
if (file.name.endsWith(".iso")) { if (file.name.endsWith(".iso")) {
setUsbMode("CDROM"); setUsbMode("CDROM");
} else if (file.name.endsWith(".img")) { } else if (file.name.endsWith(".img")) {
setUsbMode("Disk"); setUsbMode("CDROM");
} }
} }
@ -1579,6 +1579,7 @@ function UsbModeSelector({
type="radio" type="radio"
id="disk" id="disk"
name="mountType" name="mountType"
disabled
checked={usbMode === "Disk"} checked={usbMode === "Disk"}
onChange={() => setUsbMode("Disk")} onChange={() => setUsbMode("Disk")}
className="h-3 w-3 border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800" className="h-3 w-3 border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
@ -1587,6 +1588,9 @@ function UsbModeSelector({
<span className="text-sm font-medium leading-none text-slate-900 opacity-50 dark:text-white"> <span className="text-sm font-medium leading-none text-slate-900 opacity-50 dark:text-white">
Disk Disk
</span> </span>
<div className="text-[10px] text-slate-500 dark:text-slate-400">
Coming soon
</div>
</div> </div>
</label> </label>
</div> </div>

View File

@ -19,7 +19,7 @@ import { LinkButton } from "../components/Button";
import { cx } from "../cva.config"; import { cx } from "../cva.config";
import { useUiStore } from "../hooks/stores"; import { useUiStore } from "../hooks/stores";
import useKeyboard from "../hooks/useKeyboard"; import useKeyboard from "../hooks/useKeyboard";
import { useResizeObserver } from "usehooks-ts"; import { useResizeObserver } from "../hooks/useResizeObserver";
import LoadingSpinner from "../components/LoadingSpinner"; import LoadingSpinner from "../components/LoadingSpinner";
/* 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. */
@ -30,7 +30,7 @@ export default function SettingsRoute() {
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
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 } = useResizeObserver({ ref: scrollContainerRef });
// Handle scroll position to show/hide gradients // Handle scroll position to show/hide gradients
const handleScroll = () => { const handleScroll = () => {

View File

@ -5,7 +5,7 @@ import { ArrowRightIcon } from "@heroicons/react/16/solid";
import DashboardNavbar from "@components/Header"; import DashboardNavbar from "@components/Header";
import { LinkButton } from "@components/Button"; import { LinkButton } from "@components/Button";
import KvmCard from "@components/KvmCard"; import KvmCard from "@components/KvmCard";
import { useInterval } from "usehooks-ts"; import useInterval from "@/hooks/useInterval";
import { checkAuth } from "@/main"; import { checkAuth } from "@/main";
import { User } from "@/hooks/stores"; import { User } from "@/hooks/stores";
import EmptyCard from "@components/EmptyCard"; import EmptyCard from "@components/EmptyCard";

View File

@ -26,19 +26,6 @@ func writeFile(path string, data string) error {
return os.WriteFile(path, []byte(data), 0644) return os.WriteFile(path, []byte(data), 0644)
} }
func getMassStorageImage() (string, error) {
massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0")
if err != nil {
return "", fmt.Errorf("failed to get mass storage path: %w", err)
}
imagePath, err := os.ReadFile(path.Join(massStorageFunctionPath, "file"))
if err != nil {
return "", fmt.Errorf("failed to get mass storage image path: %w", err)
}
return strings.TrimSpace(string(imagePath)), nil
}
func setMassStorageImage(imagePath string) error { func setMassStorageImage(imagePath string) error {
massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0") massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0")
if err != nil { if err != nil {
@ -52,21 +39,19 @@ func setMassStorageImage(imagePath string) error {
} }
func setMassStorageMode(cdrom bool) error { func setMassStorageMode(cdrom bool) error {
massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0")
if err != nil {
return fmt.Errorf("failed to get mass storage path: %w", err)
}
mode := "0" mode := "0"
if cdrom { if cdrom {
mode = "1" mode = "1"
} }
if err := writeFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"), mode); err != nil {
err, changed := gadget.OverrideGadgetConfig("mass_storage_lun0", "cdrom", mode)
if err != nil {
return fmt.Errorf("failed to set cdrom mode: %w", err) return fmt.Errorf("failed to set cdrom mode: %w", err)
} }
return nil
if !changed {
return nil
}
return gadget.UpdateGadgetConfig()
} }
func onDiskMessage(msg webrtc.DataChannelMessage) { func onDiskMessage(msg webrtc.DataChannelMessage) {
@ -128,17 +113,20 @@ func rpcMountBuiltInImage(filename string) error {
return mountImage(imagePath) return mountImage(imagePath)
} }
func getMassStorageCDROMEnabled() (bool, error) { func getMassStorageMode() (bool, error) {
massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0") massStorageFunctionPath, err := gadget.GetPath("mass_storage_lun0")
if err != nil { if err != nil {
return false, fmt.Errorf("failed to get mass storage path: %w", err) return false, fmt.Errorf("failed to get mass storage path: %w", err)
} }
data, err := os.ReadFile(path.Join(massStorageFunctionPath, "cdrom"))
data, err := os.ReadFile(path.Join(massStorageFunctionPath, "lun.0", "cdrom"))
if err != nil { if err != nil {
return false, fmt.Errorf("failed to read cdrom mode: %w", err) return false, fmt.Errorf("failed to read cdrom mode: %w", err)
} }
// Trim any whitespace characters. It has a newline at the end // Trim any whitespace characters. It has a newline at the end
trimmedData := strings.TrimSpace(string(data)) trimmedData := strings.TrimSpace(string(data))
return trimmedData == "1", nil return trimmedData == "1", nil
} }
@ -203,60 +191,6 @@ func rpcUnmountImage() error {
var httpRangeReader *httpreadat.RangeReader var httpRangeReader *httpreadat.RangeReader
func getInitialVirtualMediaState() (*VirtualMediaState, error) {
cdromEnabled, err := getMassStorageCDROMEnabled()
if err != nil {
return nil, fmt.Errorf("failed to get mass storage cdrom enabled: %w", err)
}
diskPath, err := getMassStorageImage()
if err != nil {
return nil, fmt.Errorf("failed to get mass storage image: %w", err)
}
initialState := &VirtualMediaState{
Source: Storage,
Mode: Disk,
}
if cdromEnabled {
initialState.Mode = CDROM
}
// TODO: check if it's WebRTC or HTTP
if diskPath == "" {
return nil, nil
} else if diskPath == "/dev/nbd0" {
initialState.Source = HTTP
initialState.URL = "/"
initialState.Size = 1
} else {
initialState.Filename = filepath.Base(diskPath)
// get size from file
logger.Info().Str("diskPath", diskPath).Msg("getting file size")
info, err := os.Stat(diskPath)
if err != nil {
return nil, fmt.Errorf("failed to get file info: %w", err)
}
initialState.Size = info.Size()
}
return initialState, nil
}
func setInitialVirtualMediaState() error {
virtualMediaStateMutex.Lock()
defer virtualMediaStateMutex.Unlock()
initialState, err := getInitialVirtualMediaState()
if err != nil {
return fmt.Errorf("failed to get initial virtual media state: %w", err)
}
currentVirtualMediaState = initialState
logger.Info().Interface("initial_virtual_media_state", initialState).Msg("initial virtual media state set")
return nil
}
func rpcMountWithHTTP(url string, mode VirtualMediaMode) error { func rpcMountWithHTTP(url string, mode VirtualMediaMode) error {
virtualMediaStateMutex.Lock() virtualMediaStateMutex.Lock()
if currentVirtualMediaState != nil { if currentVirtualMediaState != nil {
@ -270,11 +204,6 @@ func rpcMountWithHTTP(url string, mode VirtualMediaMode) error {
return fmt.Errorf("failed to use http url: %w", err) return fmt.Errorf("failed to use http url: %w", err)
} }
logger.Info().Str("url", url).Int64("size", n).Msg("using remote url") logger.Info().Str("url", url).Int64("size", n).Msg("using remote url")
if err := setMassStorageMode(mode == CDROM); err != nil {
return fmt.Errorf("failed to set mass storage mode: %w", err)
}
currentVirtualMediaState = &VirtualMediaState{ currentVirtualMediaState = &VirtualMediaState{
Source: HTTP, Source: HTTP,
Mode: mode, Mode: mode,
@ -314,11 +243,6 @@ func rpcMountWithWebRTC(filename string, size int64, mode VirtualMediaMode) erro
Size: size, Size: size,
} }
virtualMediaStateMutex.Unlock() virtualMediaStateMutex.Unlock()
if err := setMassStorageMode(mode == CDROM); err != nil {
return fmt.Errorf("failed to set mass storage mode: %w", err)
}
logger.Debug().Interface("currentVirtualMediaState", currentVirtualMediaState).Msg("currentVirtualMediaState") logger.Debug().Interface("currentVirtualMediaState", currentVirtualMediaState).Msg("currentVirtualMediaState")
logger.Debug().Msg("Starting nbd device") logger.Debug().Msg("Starting nbd device")
nbdDevice = NewNBDDevice() nbdDevice = NewNBDDevice()
@ -356,10 +280,6 @@ func rpcMountWithStorage(filename string, mode VirtualMediaMode) error {
return fmt.Errorf("failed to get file info: %w", err) return fmt.Errorf("failed to get file info: %w", err)
} }
if err := setMassStorageMode(mode == CDROM); err != nil {
return fmt.Errorf("failed to set mass storage mode: %w", err)
}
err = setMassStorageImage(fullPath) err = setMassStorageImage(fullPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to set mass storage image: %w", err) return fmt.Errorf("failed to set mass storage image: %w", err)