From 3d770e73c878c3362d5c52131d279b09f0a56bb2 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Tue, 26 Aug 2025 21:40:20 -0500 Subject: [PATCH 1/2] chore/Deprecate browser mount No longer supported. --- ui/package-lock.json | 8 +- ui/package.json | 2 +- ui/src/components/popovers/MountPopover.tsx | 77 +-------- ui/src/hooks/stores.ts | 2 +- ui/src/routes/devices.$id.mount.tsx | 173 +------------------- 5 files changed, 13 insertions(+), 249 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 51c1642..1f53f37 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -49,7 +49,7 @@ "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.12", "@types/react": "^19.1.11", - "@types/react-dom": "^19.1.7", + "@types/react-dom": "^19.1.8", "@types/semver": "^7.7.0", "@types/validator": "^13.15.2", "@typescript-eslint/eslint-plugin": "^8.41.0", @@ -1970,9 +1970,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.1.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", - "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.8.tgz", + "integrity": "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ==", "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" diff --git a/ui/package.json b/ui/package.json index 2ceaf30..51de695 100644 --- a/ui/package.json +++ b/ui/package.json @@ -60,7 +60,7 @@ "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.12", "@types/react": "^19.1.11", - "@types/react-dom": "^19.1.7", + "@types/react-dom": "^19.1.8", "@types/semver": "^7.7.0", "@types/validator": "^13.15.2", "@typescript-eslint/eslint-plugin": "^8.41.0", diff --git a/ui/src/components/popovers/MountPopover.tsx b/ui/src/components/popovers/MountPopover.tsx index 1ed57d1..1381293 100644 --- a/ui/src/components/popovers/MountPopover.tsx +++ b/ui/src/components/popovers/MountPopover.tsx @@ -1,9 +1,6 @@ -import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { PlusCircleIcon } from "@heroicons/react/20/solid"; -import { useMemo, forwardRef, useEffect, useCallback } from "react"; +import { forwardRef, useEffect, useCallback } from "react"; import { - LuArrowUpFromLine, - LuCheckCheck, LuLink, LuPlus, LuRadioReceiver, @@ -14,38 +11,17 @@ import { useLocation } from "react-router-dom"; import { Button } from "@components/Button"; import Card, { GridCard } from "@components/Card"; import { formatters } from "@/utils"; -import { RemoteVirtualMediaState, useMountMediaStore, useRTCStore } from "@/hooks/stores"; +import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import notifications from "@/notifications"; const MountPopopover = forwardRef((_props, ref) => { - const { diskDataChannelStats } = useRTCStore(); const { send } = useJsonRpc(); const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } = useMountMediaStore(); - const bytesSentPerSecond = useMemo(() => { - if (diskDataChannelStats.size < 2) return null; - - const secondLastItem = - Array.from(diskDataChannelStats)[diskDataChannelStats.size - 2]; - const lastItem = Array.from(diskDataChannelStats)[diskDataChannelStats.size - 1]; - - if (!secondLastItem || !lastItem) return 0; - - const lastTime = lastItem[0]; - const secondLastTime = secondLastItem[0]; - const timeDelta = lastTime - secondLastTime; - - const lastBytesSent = lastItem[1].bytesSent; - const secondLastBytesSent = secondLastItem[1].bytesSent; - const bytesDelta = lastBytesSent - secondLastBytesSent; - - return bytesDelta / timeDelta; - }, [diskDataChannelStats]); - const syncRemoteVirtualMediaState = useCallback(() => { send("getVirtualMediaState", {}, (response: JsonRpcResponse) => { if ("error" in response) { @@ -94,42 +70,6 @@ const MountPopopover = forwardRef((_props, ref) => { const { source, filename, size, url, path } = remoteVirtualMediaState; switch (source) { - case "WebRTC": - return ( - <> -
-
- -

- Streaming from Browser -

-
- -
- {formatters.truncateMiddle(filename, 50)} -
-
-
-
-
-
- {formatters.bytes(size ?? 0)} -
- - - {bytesSentPerSecond !== null - ? `${formatters.bytes(bytesSentPerSecond)}/s` - : "N/A"} - -
-
-
-
- - ); case "HTTP": return (
@@ -202,18 +142,7 @@ const MountPopopover = forwardRef((_props, ref) => { description="Mount an image to boot from or install an operating system." /> - {remoteVirtualMediaState?.source === "WebRTC" ? ( - -
- -
-
Closing this tab will unmount the image
-
-
-
- ) : null} - -
void; - modalView: "mode" | "browser" | "url" | "device" | "upload" | "error" | null; + modalView: "mode" | "url" | "device" | "upload" | "error" | null; setModalView: (view: MountMediaState["modalView"]) => void; isMountMediaDialogOpen: boolean; diff --git a/ui/src/routes/devices.$id.mount.tsx b/ui/src/routes/devices.$id.mount.tsx index 295429f..3361f38 100644 --- a/ui/src/routes/devices.$id.mount.tsx +++ b/ui/src/routes/devices.$id.mount.tsx @@ -1,9 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { - LuGlobe, LuLink, LuRadioReceiver, - LuHardDrive, LuCheck, LuUpload, } from "react-icons/lu"; @@ -131,35 +129,7 @@ export function Dialog({ onClose }: { onClose: () => void }) { clearMountMediaState(); } - function handleBrowserMount(file: File, mode: RemoteVirtualMediaState["mode"]) { - console.log(`Mounting ${file.name} as ${mode}`); - - setMountInProgress(true); - send( - "mountWithWebRTC", - { filename: file.name, size: file.size, mode }, - async resp => { - if ("error" in resp) triggerError(resp.error.message); - - clearMountMediaState(); - syncRemoteVirtualMediaState() - .then(() => { - // We need to keep the local file in the store so that the browser can - // continue to stream the file to the device - setLocalFile(file); - navigate(".."); - }) - .catch(err => { - triggerError(err instanceof Error ? err.message : String(err)); - }) - .finally(() => { - setMountInProgress(false); - }); - }, - ); - } - - const [selectedMode, setSelectedMode] = useState<"browser" | "url" | "device">("url"); + const [selectedMode, setSelectedMode] = useState<"url" | "device">("url"); return (
void }) { "max-w-4xl": modalView === "mode", "max-w-2xl": modalView === "device", "max-w-xl": - modalView === "browser" || modalView === "url" || modalView === "upload" || modalView === "error", @@ -194,19 +163,6 @@ export function Dialog({ onClose }: { onClose: () => void }) { /> )} - {modalView === "browser" && ( - { - handleBrowserMount(file, mode); - }} - onBack={() => { - setMountInProgress(false); - setModalView("mode"); - }} - /> - )} - {modalView === "url" && ( void; - selectedMode: "browser" | "url" | "device"; - setSelectedMode: (mode: "browser" | "url" | "device") => void; + selectedMode: "url" | "device"; + setSelectedMode: (mode: "url" | "device") => void; }) { const { setModalView } = useMountMediaStore(); @@ -292,14 +248,6 @@ function ModeSelectionView({
{[ - { - label: "Browser Mount", - value: "browser", - description: "Stream files directly from your browser", - icon: LuGlobe, - tag: "Coming Soon", - disabled: true, - }, { label: "URL Mount", value: "url", @@ -338,7 +286,7 @@ function ModeSelectionView({
- disabled ? null : setSelectedMode(mode as "browser" | "url" | "device") + disabled ? null : setSelectedMode(mode as "url" | "device") } >
@@ -394,119 +342,6 @@ function ModeSelectionView({ ); } -function BrowserFileView({ - onMountFile, - onBack, - mountInProgress, -}: { - onBack: () => void; - onMountFile: (file: File, mode: RemoteVirtualMediaState["mode"]) => void; - mountInProgress: boolean; -}) { - const [selectedFile, setSelectedFile] = useState(null); - const [usbMode, setUsbMode] = useState("CDROM"); - - const handleFileChange = (event: React.ChangeEvent) => { - const file = event.target.files?.[0] || null; - setSelectedFile(file); - - if (file?.name.endsWith(".iso")) { - setUsbMode("CDROM"); - } else if (file?.name.endsWith(".img")) { - setUsbMode("Disk"); - } - }; - - const handleMount = () => { - if (selectedFile) { - console.log(`Mounting ${selectedFile.name} as ${setUsbMode}`); - onMountFile(selectedFile, usbMode); - } - }; - - return ( -
- -
-
document.getElementById("file-upload")?.click()} - className="block cursor-pointer select-none" - > -
- -
-
- {selectedFile ? ( - <> -
- -

- {formatters.truncateMiddle(selectedFile.name, 40)} -

-

- {formatters.bytes(selectedFile.size)} -

-
- - ) : ( -
- -

- Click to select a file -

-

- Supported formats: ISO, IMG -

-
- )} -
-
-
-
-
- -
- -
-
- -
-
-
-
-
- ); -} - function UrlView({ onBack, onMount, From 2245dadae3a625416d3ef034fd50d72dc14d3a7f Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Thu, 28 Aug 2025 00:15:07 +0000 Subject: [PATCH 2/2] Remove device-side go code --- block_device.go | 11 +---- fuse.go | 114 -------------------------------------------- jsonrpc.go | 1 - remote_mount.go | 62 ------------------------ usb_mass_storage.go | 43 ----------------- webrtc.go | 3 -- 6 files changed, 2 insertions(+), 232 deletions(-) delete mode 100644 fuse.go delete mode 100644 remote_mount.go diff --git a/block_device.go b/block_device.go index 2274098..f20b65d 100644 --- a/block_device.go +++ b/block_device.go @@ -25,22 +25,15 @@ func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) { mountedImageSize := currentVirtualMediaState.Size virtualMediaStateMutex.RUnlock() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + _, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() readLen := int64(len(p)) if off+readLen > mountedImageSize { readLen = mountedImageSize - off } - var data []byte + switch source { - case WebRTC: - data, err = webRTCDiskReader.Read(ctx, off, readLen) - if err != nil { - return 0, err - } - n = copy(p, data) - return n, nil case HTTP: return httpRangeReader.ReadAt(p, off) default: diff --git a/fuse.go b/fuse.go deleted file mode 100644 index 19f144f..0000000 --- a/fuse.go +++ /dev/null @@ -1,114 +0,0 @@ -package kvm - -import ( - "context" - "os" - "sync" - "syscall" - - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" -) - -type WebRTCStreamFile struct { - fs.Inode - mu sync.Mutex - Attr fuse.Attr - size uint64 -} - -var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil)) -var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil)) -var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil)) -var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil)) -var _ = (fs.NodeOpener)((*WebRTCStreamFile)(nil)) - -func (f *WebRTCStreamFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { - return nil, fuse.FOPEN_KEEP_CACHE, fs.OK -} - -func (f *WebRTCStreamFile) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) { - return 0, syscall.EROFS -} - -var _ = (fs.NodeGetattrer)((*WebRTCStreamFile)(nil)) - -func (f *WebRTCStreamFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - f.mu.Lock() - defer f.mu.Unlock() - out.Attr = f.Attr - out.Size = f.size - return fs.OK -} - -func (f *WebRTCStreamFile) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno { - f.mu.Lock() - defer f.mu.Unlock() - out.Attr = f.Attr - return fs.OK -} - -func (f *WebRTCStreamFile) Flush(ctx context.Context, fh fs.FileHandle) syscall.Errno { - return fs.OK -} - -type DiskReadRequest struct { - Start uint64 `json:"start"` - End uint64 `json:"end"` -} - -var diskReadChan = make(chan []byte, 1) - -func (f *WebRTCStreamFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { - buf, err := webRTCDiskReader.Read(ctx, off, int64(len(dest))) - if err != nil { - return nil, syscall.EIO - } - return fuse.ReadResultData(buf), fs.OK -} - -func (f *WebRTCStreamFile) SetSize(size uint64) { - f.mu.Lock() - defer f.mu.Unlock() - f.size = size -} - -type FuseRoot struct { - fs.Inode -} - -var webRTCStreamFile = &WebRTCStreamFile{} - -func (r *FuseRoot) OnAdd(ctx context.Context) { - ch := r.NewPersistentInode(ctx, webRTCStreamFile, fs.StableAttr{Ino: 2}) - r.AddChild("disk", ch, false) -} - -func (r *FuseRoot) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { - out.Mode = 0755 - return 0 -} - -var _ = (fs.NodeGetattrer)((*FuseRoot)(nil)) -var _ = (fs.NodeOnAdder)((*FuseRoot)(nil)) - -const fuseMountPoint = "/mnt/webrtc" - -var fuseServer *fuse.Server - -func RunFuseServer() { - opts := &fs.Options{} - opts.DirectMountStrict = true - _ = os.Mkdir(fuseMountPoint, 0755) - var err error - fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts) - if err != nil { - logger.Warn().Err(err).Msg("failed to mount fuse") - } - fuseServer.Wait() -} - -type WebRTCImage struct { - Size uint64 `json:"size"` - Filename string `json:"filename"` -} diff --git a/jsonrpc.go b/jsonrpc.go index 6f9c670..82b12d0 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -1103,7 +1103,6 @@ var rpcHandlers = map[string]RPCHandler{ "getVirtualMediaState": {Func: rpcGetVirtualMediaState}, "getStorageSpace": {Func: rpcGetStorageSpace}, "mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}}, - "mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}}, "mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}}, "listStorageFiles": {Func: rpcListStorageFiles}, "deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}}, diff --git a/remote_mount.go b/remote_mount.go deleted file mode 100644 index 32a0fd2..0000000 --- a/remote_mount.go +++ /dev/null @@ -1,62 +0,0 @@ -package kvm - -import ( - "context" - "encoding/json" - "errors" -) - -type RemoteImageReader interface { - Read(ctx context.Context, offset int64, size int64) ([]byte, error) -} - -type WebRTCDiskReader struct { -} - -var webRTCDiskReader WebRTCDiskReader - -func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) ([]byte, error) { - virtualMediaStateMutex.RLock() - if currentVirtualMediaState == nil { - virtualMediaStateMutex.RUnlock() - return nil, errors.New("image not mounted") - } - if currentVirtualMediaState.Source != WebRTC { - virtualMediaStateMutex.RUnlock() - return nil, errors.New("image not mounted from webrtc") - } - mountedImageSize := currentVirtualMediaState.Size - virtualMediaStateMutex.RUnlock() - end := min(offset+size, mountedImageSize) - req := DiskReadRequest{ - Start: uint64(offset), - End: uint64(end), - } - jsonBytes, err := json.Marshal(req) - if err != nil { - return nil, err - } - - if currentSession == nil || currentSession.DiskChannel == nil { - return nil, errors.New("not active session") - } - - logger.Debug().Str("request", string(jsonBytes)).Msg("reading from webrtc") - err = currentSession.DiskChannel.SendText(string(jsonBytes)) - if err != nil { - return nil, err - } - var buf []byte - for { - select { - case data := <-diskReadChan: - buf = data[16:] - case <-ctx.Done(): - return nil, context.Canceled - } - if len(buf) >= int(end-offset) { - break - } - } - return buf, nil -} diff --git a/usb_mass_storage.go b/usb_mass_storage.go index 498c311..0db4c52 100644 --- a/usb_mass_storage.go +++ b/usb_mass_storage.go @@ -69,11 +69,6 @@ func setMassStorageMode(cdrom bool) error { return gadget.UpdateGadgetConfig() } -func onDiskMessage(msg webrtc.DataChannelMessage) { - logger.Info().Int("len", len(msg.Data)).Msg("Disk Message") - diskReadChan <- msg.Data -} - func mountImage(imagePath string) error { err := setMassStorageImage("") if err != nil { @@ -234,7 +229,6 @@ func getInitialVirtualMediaState() (*VirtualMediaState, error) { initialState.Mode = CDROM } - // TODO: check if it's WebRTC or HTTP switch diskPath { case "": return nil, nil @@ -313,43 +307,6 @@ func rpcMountWithHTTP(url string, mode VirtualMediaMode) error { return nil } -func rpcMountWithWebRTC(filename string, size int64, mode VirtualMediaMode) error { - virtualMediaStateMutex.Lock() - if currentVirtualMediaState != nil { - virtualMediaStateMutex.Unlock() - return fmt.Errorf("another virtual media is already mounted") - } - currentVirtualMediaState = &VirtualMediaState{ - Source: WebRTC, - Mode: mode, - Filename: filename, - Size: size, - } - 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().Msg("Starting nbd device") - nbdDevice = NewNBDDevice() - err := nbdDevice.Start() - if err != nil { - logger.Warn().Err(err).Msg("failed to start nbd device") - return err - } - logger.Debug().Msg("nbd device started") - //TODO: replace by polling on block device having right size - time.Sleep(1 * time.Second) - err = setMassStorageImage("/dev/nbd0") - if err != nil { - return err - } - logger.Info().Msg("usb mass storage mounted") - return nil -} - func rpcMountWithStorage(filename string, mode VirtualMediaMode) error { filename, err := sanitizeFilename(filename) if err != nil { diff --git a/webrtc.go b/webrtc.go index 1d4dd94..d9a039d 100644 --- a/webrtc.go +++ b/webrtc.go @@ -126,9 +126,6 @@ func newSession(config SessionConfig) (*Session, error) { triggerOTAStateUpdate() triggerVideoStateUpdate() triggerUSBStateUpdate() - case "disk": - session.DiskChannel = d - d.OnMessage(onDiskMessage) case "terminal": handleTerminalChannel(d) case "serial":