mirror of https://github.com/jetkvm/kvm.git
Compare commits
12 Commits
8234daf0a1
...
d0b4062bb2
Author | SHA1 | Date |
---|---|---|
|
d0b4062bb2 | |
|
94521ef6db | |
|
57fbee1490 | |
|
0e65c0a9a9 | |
|
2dafb5c9c1 | |
|
566305549f | |
|
1505c37e4c | |
|
564eee9b00 | |
|
fab575dbe0 | |
|
97958e7b86 | |
|
2f7042df18 | |
|
2cadda4e00 |
|
@ -0,0 +1,126 @@
|
||||||
|
name: Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-22.04]
|
||||||
|
go: [1.21, 1.23.4]
|
||||||
|
node: [21]
|
||||||
|
goos: [linux]
|
||||||
|
goarch: [arm]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
working-directory: ui
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build UI
|
||||||
|
working-directory: ui
|
||||||
|
run: npm run build:device
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Install Go Dependencies
|
||||||
|
run: |
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
- name: Build Binaries
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
run: |
|
||||||
|
GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w -X kvm.builtAppVersion=dev-${GIT_COMMIT:0:7}" -o bin/jetkvm_app cmd/main.go
|
||||||
|
chmod 755 bin/jetkvm_app
|
||||||
|
|
||||||
|
- name: Upload Debug Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ (github.ref == 'refs/heads/main' || github.event_name == 'pull_request') && matrix.go == '1.21' }}
|
||||||
|
with:
|
||||||
|
name: jetkvm_app_debug
|
||||||
|
path: bin/jetkvm_app
|
||||||
|
|
||||||
|
comment:
|
||||||
|
name: Comment
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Generate Links
|
||||||
|
id: linksa
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[0].id')
|
||||||
|
echo "ARTIFACT_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID" >> $GITHUB_ENV
|
||||||
|
echo "LATEST_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Comment on PR
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
|
TITLE="${{ github.event.pull_request.title }}"
|
||||||
|
PR_NUMBER=${{ github.event.pull_request.number }}
|
||||||
|
else
|
||||||
|
TITLE="main branch"
|
||||||
|
fi
|
||||||
|
|
||||||
|
COMMENT=$(cat << EOF
|
||||||
|
✅ **Build successfully for $TITLE!**
|
||||||
|
|
||||||
|
| Name | Link |
|
||||||
|
|------------------|----------------------------------------------------------------------|
|
||||||
|
| 🔗 Debug Binary | [Download](${{ env.ARTIFACT_URL }}) |
|
||||||
|
| 🔗 Latest commit | [${{ env.LATEST_COMMIT }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}) |
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Post Comment
|
||||||
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
|
# Look for an existing comment
|
||||||
|
COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/$PR_NUMBER/comments \
|
||||||
|
--jq '.[] | select(.body | contains("✅ **Build successfully for")) | .id')
|
||||||
|
|
||||||
|
if [ -z "$COMMENT_ID" ]; then
|
||||||
|
# Create a new comment if none exists
|
||||||
|
gh pr comment $PR_NUMBER --body "$COMMENT"
|
||||||
|
else
|
||||||
|
# Update the existing comment
|
||||||
|
gh api repos/${{ github.repository }}/issues/comments/$COMMENT_ID \
|
||||||
|
--method PATCH \
|
||||||
|
-f body="$COMMENT"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Log the comment for main branch
|
||||||
|
echo "$COMMENT"
|
||||||
|
fi
|
|
@ -0,0 +1,91 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 21
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
working-directory: ui
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build UI
|
||||||
|
working-directory: ui
|
||||||
|
run: npm run build:device
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: 1.21
|
||||||
|
|
||||||
|
- name: Build Release Binaries
|
||||||
|
env:
|
||||||
|
REF: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w -X kvm.builtAppVersion=${REF:11}" -o bin/jetkvm_app cmd/main.go
|
||||||
|
chmod 755 bin/jetkvm_app
|
||||||
|
|
||||||
|
- name: Create checksum
|
||||||
|
env:
|
||||||
|
REF: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
SUM=$(shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1)
|
||||||
|
echo -e "\n#### SHA256 Checksum\n\`\`\`\n$SUM bin/jetkvm_app\n\`\`\`\n" >> ./RELEASE_CHANGELOG
|
||||||
|
echo -e "$SUM bin/jetkvm_app\n" > checksums.txt
|
||||||
|
|
||||||
|
- name: Create Release Branch
|
||||||
|
env:
|
||||||
|
REF: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
BRANCH=release/${REF:10}
|
||||||
|
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --local user.name "github-actions[bot]"
|
||||||
|
git checkout -b ${BRANCH}
|
||||||
|
git push -u origin ${BRANCH}
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
||||||
|
body_path: ./RELEASE_CHANGELOG
|
||||||
|
|
||||||
|
- name: Upload JetKVM binary
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: bin/jetkvm_app
|
||||||
|
asset_name: jetkvm_app
|
||||||
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
|
- name: Upload checksum
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./checksums.txt
|
||||||
|
asset_name: checksums.txt
|
||||||
|
asset_content_type: text/plain
|
|
@ -22,25 +22,12 @@ func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) {
|
||||||
return 0, errors.New("image not mounted")
|
return 0, errors.New("image not mounted")
|
||||||
}
|
}
|
||||||
source := currentVirtualMediaState.Source
|
source := currentVirtualMediaState.Source
|
||||||
mountedImageSize := currentVirtualMediaState.Size
|
|
||||||
virtualMediaStateMutex.RUnlock()
|
virtualMediaStateMutex.RUnlock()
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
_, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
readLen := int64(len(p))
|
|
||||||
if off+readLen > mountedImageSize {
|
|
||||||
readLen = mountedImageSize - off
|
|
||||||
}
|
|
||||||
var data []byte
|
|
||||||
switch source {
|
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:
|
case HTTP:
|
||||||
return httpRangeReader.ReadAt(p, off)
|
return httpRangeReader.ReadAt(p, off)
|
||||||
default:
|
default:
|
||||||
|
|
114
fuse.go
114
fuse.go
|
@ -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"`
|
|
||||||
}
|
|
|
@ -1103,7 +1103,6 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"getVirtualMediaState": {Func: rpcGetVirtualMediaState},
|
"getVirtualMediaState": {Func: rpcGetVirtualMediaState},
|
||||||
"getStorageSpace": {Func: rpcGetStorageSpace},
|
"getStorageSpace": {Func: rpcGetStorageSpace},
|
||||||
"mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
|
"mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
|
||||||
"mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}},
|
|
||||||
"mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
|
"mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
|
||||||
"listStorageFiles": {Func: rpcListStorageFiles},
|
"listStorageFiles": {Func: rpcListStorageFiles},
|
||||||
"deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}},
|
"deleteStorageFile": {Func: rpcDeleteStorageFile, Params: []string{"filename"}},
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -49,7 +49,7 @@
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tailwindcss/vite": "^4.1.12",
|
"@tailwindcss/vite": "^4.1.12",
|
||||||
"@types/react": "^19.1.11",
|
"@types/react": "^19.1.11",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.8",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.0",
|
||||||
"@types/validator": "^13.15.2",
|
"@types/validator": "^13.15.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||||
|
@ -1970,9 +1970,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-dom": {
|
"node_modules/@types/react-dom": {
|
||||||
"version": "19.1.7",
|
"version": "19.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.8.tgz",
|
||||||
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
"integrity": "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tailwindcss/vite": "^4.1.12",
|
"@tailwindcss/vite": "^4.1.12",
|
||||||
"@types/react": "^19.1.11",
|
"@types/react": "^19.1.11",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.8",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.0",
|
||||||
"@types/validator": "^13.15.2",
|
"@types/validator": "^13.15.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
import { PlusCircleIcon } from "@heroicons/react/20/solid";
|
||||||
import { useMemo, forwardRef, useEffect, useCallback } from "react";
|
import { forwardRef, useEffect, useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
LuArrowUpFromLine,
|
|
||||||
LuCheckCheck,
|
|
||||||
LuLink,
|
LuLink,
|
||||||
LuPlus,
|
LuPlus,
|
||||||
LuRadioReceiver,
|
LuRadioReceiver,
|
||||||
|
@ -14,38 +11,17 @@ import { useLocation } from "react-router-dom";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Card, { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
import { formatters } from "@/utils";
|
import { formatters } from "@/utils";
|
||||||
import { RemoteVirtualMediaState, useMountMediaStore, useRTCStore } from "@/hooks/stores";
|
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
const { diskDataChannelStats } = useRTCStore();
|
|
||||||
const { send } = useJsonRpc();
|
const { send } = useJsonRpc();
|
||||||
const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } =
|
const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } =
|
||||||
useMountMediaStore();
|
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(() => {
|
const syncRemoteVirtualMediaState = useCallback(() => {
|
||||||
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
|
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
|
||||||
if ("error" in response) {
|
if ("error" in response) {
|
||||||
|
@ -94,42 +70,6 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
const { source, filename, size, url, path } = remoteVirtualMediaState;
|
const { source, filename, size, url, path } = remoteVirtualMediaState;
|
||||||
|
|
||||||
switch (source) {
|
switch (source) {
|
||||||
case "WebRTC":
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<LuCheckCheck className="h-5 text-green-500" />
|
|
||||||
<h3 className="text-base font-semibold text-black dark:text-white">
|
|
||||||
Streaming from Browser
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<Card className="w-auto px-2 py-1">
|
|
||||||
<div className="w-full truncate text-sm text-black dark:text-white">
|
|
||||||
{formatters.truncateMiddle(filename, 50)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="my-2 flex flex-col items-center gap-y-2">
|
|
||||||
<div className="w-full text-sm text-slate-900 dark:text-slate-100">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span>{formatters.bytes(size ?? 0)}</span>
|
|
||||||
<div className="flex items-center gap-x-1">
|
|
||||||
<LuArrowUpFromLine
|
|
||||||
className="h-4 text-blue-700 dark:text-blue-500"
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
{bytesSentPerSecond !== null
|
|
||||||
? `${formatters.bytes(bytesSentPerSecond)}/s`
|
|
||||||
: "N/A"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case "HTTP":
|
case "HTTP":
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
|
@ -202,18 +142,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
description="Mount an image to boot from or install an operating system."
|
description="Mount an image to boot from or install an operating system."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{remoteVirtualMediaState?.source === "WebRTC" ? (
|
<div
|
||||||
<Card>
|
|
||||||
<div className="flex items-center gap-x-1.5 px-2.5 py-2 text-sm">
|
|
||||||
<ExclamationTriangleIcon className="h-4 text-yellow-500" />
|
|
||||||
<div className="flex w-full items-center text-black">
|
|
||||||
<div>Closing this tab will unmount the image</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="animate-fadeIn opacity-0 space-y-2"
|
className="animate-fadeIn opacity-0 space-y-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
|
|
|
@ -105,9 +105,6 @@ export interface RTCState {
|
||||||
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
||||||
rpcDataChannel: RTCDataChannel | null;
|
rpcDataChannel: RTCDataChannel | null;
|
||||||
|
|
||||||
diskChannel: RTCDataChannel | null;
|
|
||||||
setDiskChannel: (channel: RTCDataChannel) => void;
|
|
||||||
|
|
||||||
peerConnectionState: RTCPeerConnectionState | null;
|
peerConnectionState: RTCPeerConnectionState | null;
|
||||||
setPeerConnectionState: (state: RTCPeerConnectionState) => void;
|
setPeerConnectionState: (state: RTCPeerConnectionState) => void;
|
||||||
|
|
||||||
|
@ -160,9 +157,6 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
peerConnectionState: null,
|
peerConnectionState: null,
|
||||||
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
|
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
|
||||||
|
|
||||||
diskChannel: null,
|
|
||||||
setDiskChannel: (channel: RTCDataChannel) => set({ diskChannel: channel }),
|
|
||||||
|
|
||||||
mediaStream: null,
|
mediaStream: null,
|
||||||
setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }),
|
setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }),
|
||||||
|
|
||||||
|
@ -381,7 +375,7 @@ export const useSettingsStore = create(
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface RemoteVirtualMediaState {
|
export interface RemoteVirtualMediaState {
|
||||||
source: "WebRTC" | "HTTP" | "Storage" | null;
|
source: "HTTP" | "Storage" | null;
|
||||||
mode: "CDROM" | "Disk" | null;
|
mode: "CDROM" | "Disk" | null;
|
||||||
filename: string | null;
|
filename: string | null;
|
||||||
url: string | null;
|
url: string | null;
|
||||||
|
@ -390,13 +384,10 @@ export interface RemoteVirtualMediaState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MountMediaState {
|
export interface MountMediaState {
|
||||||
localFile: File | null;
|
|
||||||
setLocalFile: (file: MountMediaState["localFile"]) => void;
|
|
||||||
|
|
||||||
remoteVirtualMediaState: RemoteVirtualMediaState | null;
|
remoteVirtualMediaState: RemoteVirtualMediaState | null;
|
||||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => void;
|
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => void;
|
||||||
|
|
||||||
modalView: "mode" | "browser" | "url" | "device" | "upload" | "error" | null;
|
modalView: "mode" | "url" | "device" | "upload" | "error" | null;
|
||||||
setModalView: (view: MountMediaState["modalView"]) => void;
|
setModalView: (view: MountMediaState["modalView"]) => void;
|
||||||
|
|
||||||
isMountMediaDialogOpen: boolean;
|
isMountMediaDialogOpen: boolean;
|
||||||
|
@ -410,9 +401,6 @@ export interface MountMediaState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMountMediaStore = create<MountMediaState>(set => ({
|
export const useMountMediaStore = create<MountMediaState>(set => ({
|
||||||
localFile: null,
|
|
||||||
setLocalFile: (file: MountMediaState["localFile"]) => set({ localFile: file }),
|
|
||||||
|
|
||||||
remoteVirtualMediaState: null,
|
remoteVirtualMediaState: null,
|
||||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }),
|
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }),
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LuGlobe,
|
|
||||||
LuLink,
|
LuLink,
|
||||||
LuRadioReceiver,
|
LuRadioReceiver,
|
||||||
LuHardDrive,
|
|
||||||
LuCheck,
|
LuCheck,
|
||||||
LuUpload,
|
LuUpload,
|
||||||
} from "react-icons/lu";
|
} from "react-icons/lu";
|
||||||
|
@ -50,7 +48,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||||
const {
|
const {
|
||||||
modalView,
|
modalView,
|
||||||
setModalView,
|
setModalView,
|
||||||
setLocalFile,
|
|
||||||
setRemoteVirtualMediaState,
|
setRemoteVirtualMediaState,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
setErrorMessage,
|
setErrorMessage,
|
||||||
|
@ -60,7 +57,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||||
const [incompleteFileName, setIncompleteFileName] = useState<string | null>(null);
|
const [incompleteFileName, setIncompleteFileName] = useState<string | null>(null);
|
||||||
const [mountInProgress, setMountInProgress] = useState(false);
|
const [mountInProgress, setMountInProgress] = useState(false);
|
||||||
function clearMountMediaState() {
|
function clearMountMediaState() {
|
||||||
setLocalFile(null);
|
|
||||||
setRemoteVirtualMediaState(null);
|
setRemoteVirtualMediaState(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,35 +127,7 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||||
clearMountMediaState();
|
clearMountMediaState();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBrowserMount(file: File, mode: RemoteVirtualMediaState["mode"]) {
|
const [selectedMode, setSelectedMode] = useState<"url" | "device">("url");
|
||||||
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");
|
|
||||||
return (
|
return (
|
||||||
<AutoHeight>
|
<AutoHeight>
|
||||||
<div
|
<div
|
||||||
|
@ -167,7 +135,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||||
"max-w-4xl": modalView === "mode",
|
"max-w-4xl": modalView === "mode",
|
||||||
"max-w-2xl": modalView === "device",
|
"max-w-2xl": modalView === "device",
|
||||||
"max-w-xl":
|
"max-w-xl":
|
||||||
modalView === "browser" ||
|
|
||||||
modalView === "url" ||
|
modalView === "url" ||
|
||||||
modalView === "upload" ||
|
modalView === "upload" ||
|
||||||
modalView === "error",
|
modalView === "error",
|
||||||
|
@ -194,19 +161,6 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{modalView === "browser" && (
|
|
||||||
<BrowserFileView
|
|
||||||
mountInProgress={mountInProgress}
|
|
||||||
onMountFile={(file, mode) => {
|
|
||||||
handleBrowserMount(file, mode);
|
|
||||||
}}
|
|
||||||
onBack={() => {
|
|
||||||
setMountInProgress(false);
|
|
||||||
setModalView("mode");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modalView === "url" && (
|
{modalView === "url" && (
|
||||||
<UrlView
|
<UrlView
|
||||||
mountInProgress={mountInProgress}
|
mountInProgress={mountInProgress}
|
||||||
|
@ -275,8 +229,8 @@ function ModeSelectionView({
|
||||||
setSelectedMode,
|
setSelectedMode,
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
selectedMode: "browser" | "url" | "device";
|
selectedMode: "url" | "device";
|
||||||
setSelectedMode: (mode: "browser" | "url" | "device") => void;
|
setSelectedMode: (mode: "url" | "device") => void;
|
||||||
}) {
|
}) {
|
||||||
const { setModalView } = useMountMediaStore();
|
const { setModalView } = useMountMediaStore();
|
||||||
|
|
||||||
|
@ -292,14 +246,6 @@ function ModeSelectionView({
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
{[
|
{[
|
||||||
{
|
|
||||||
label: "Browser Mount",
|
|
||||||
value: "browser",
|
|
||||||
description: "Stream files directly from your browser",
|
|
||||||
icon: LuGlobe,
|
|
||||||
tag: "Coming Soon",
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "URL Mount",
|
label: "URL Mount",
|
||||||
value: "url",
|
value: "url",
|
||||||
|
@ -338,7 +284,7 @@ function ModeSelectionView({
|
||||||
<div
|
<div
|
||||||
className="relative z-50 flex flex-col items-start p-4 select-none"
|
className="relative z-50 flex flex-col items-start p-4 select-none"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device")
|
disabled ? null : setSelectedMode(mode as "url" | "device")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
@ -394,119 +340,6 @@ function ModeSelectionView({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BrowserFileView({
|
|
||||||
onMountFile,
|
|
||||||
onBack,
|
|
||||||
mountInProgress,
|
|
||||||
}: {
|
|
||||||
onBack: () => void;
|
|
||||||
onMountFile: (file: File, mode: RemoteVirtualMediaState["mode"]) => void;
|
|
||||||
mountInProgress: boolean;
|
|
||||||
}) {
|
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
||||||
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
|
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
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 (
|
|
||||||
<div className="w-full space-y-4">
|
|
||||||
<ViewHeader
|
|
||||||
title="Mount from Browser"
|
|
||||||
description="Select an image file to mount"
|
|
||||||
/>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div
|
|
||||||
onClick={() => document.getElementById("file-upload")?.click()}
|
|
||||||
className="block cursor-pointer select-none"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="group animate-fadeIn opacity-0"
|
|
||||||
style={{
|
|
||||||
animationDuration: "0.7s",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card className="transition-all duration-300 outline-dashed">
|
|
||||||
<div className="w-full px-4 py-12">
|
|
||||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
|
||||||
{selectedFile ? (
|
|
||||||
<>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<LuHardDrive className="mx-auto h-6 w-6 text-blue-700" />
|
|
||||||
<h3 className="text-sm leading-none font-semibold">
|
|
||||||
{formatters.truncateMiddle(selectedFile.name, 40)}
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs leading-none text-slate-700">
|
|
||||||
{formatters.bytes(selectedFile.size)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700" />
|
|
||||||
<h3 className="text-sm leading-none font-semibold">
|
|
||||||
Click to select a file
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs leading-none text-slate-700">
|
|
||||||
Supported formats: ISO, IMG
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="file-upload"
|
|
||||||
type="file"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
className="hidden"
|
|
||||||
accept=".iso, .img"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
|
|
||||||
style={{
|
|
||||||
animationDuration: "0.7s",
|
|
||||||
animationDelay: "0.1s",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Fieldset disabled={!selectedFile}>
|
|
||||||
<UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} />
|
|
||||||
</Fieldset>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button size="MD" theme="blank" text="Back" onClick={onBack} />
|
|
||||||
<Button
|
|
||||||
size="MD"
|
|
||||||
theme="primary"
|
|
||||||
text="Mount File"
|
|
||||||
onClick={handleMount}
|
|
||||||
disabled={!selectedFile || mountInProgress}
|
|
||||||
loading={mountInProgress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UrlView({
|
function UrlView({
|
||||||
onBack,
|
onBack,
|
||||||
onMount,
|
onMount,
|
||||||
|
|
|
@ -29,7 +29,6 @@ import {
|
||||||
USBStates,
|
USBStates,
|
||||||
useDeviceStore,
|
useDeviceStore,
|
||||||
useHidStore,
|
useHidStore,
|
||||||
useMountMediaStore,
|
|
||||||
useNetworkStateStore,
|
useNetworkStateStore,
|
||||||
User,
|
User,
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
|
@ -132,7 +131,6 @@ export default function KvmIdRoute() {
|
||||||
const {
|
const {
|
||||||
peerConnection, setPeerConnection,
|
peerConnection, setPeerConnection,
|
||||||
peerConnectionState, setPeerConnectionState,
|
peerConnectionState, setPeerConnectionState,
|
||||||
diskChannel, setDiskChannel,
|
|
||||||
setMediaStream,
|
setMediaStream,
|
||||||
setRpcDataChannel,
|
setRpcDataChannel,
|
||||||
isTurnServerInUse, setTurnServerInUse,
|
isTurnServerInUse, setTurnServerInUse,
|
||||||
|
@ -484,18 +482,12 @@ export default function KvmIdRoute() {
|
||||||
setRpcDataChannel(rpcDataChannel);
|
setRpcDataChannel(rpcDataChannel);
|
||||||
};
|
};
|
||||||
|
|
||||||
const diskDataChannel = pc.createDataChannel("disk");
|
|
||||||
diskDataChannel.onopen = () => {
|
|
||||||
setDiskChannel(diskDataChannel);
|
|
||||||
};
|
|
||||||
|
|
||||||
setPeerConnection(pc);
|
setPeerConnection(pc);
|
||||||
}, [
|
}, [
|
||||||
cleanupAndStopReconnecting,
|
cleanupAndStopReconnecting,
|
||||||
iceConfig?.iceServers,
|
iceConfig?.iceServers,
|
||||||
legacyHTTPSignaling,
|
legacyHTTPSignaling,
|
||||||
sendWebRTCSignal,
|
sendWebRTCSignal,
|
||||||
setDiskChannel,
|
|
||||||
setMediaStream,
|
setMediaStream,
|
||||||
setPeerConnection,
|
setPeerConnection,
|
||||||
setPeerConnectionState,
|
setPeerConnectionState,
|
||||||
|
@ -719,25 +711,6 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
}, [navigate, navigateTo, queryParams, setModalView, setQueryParams]);
|
}, [navigate, navigateTo, queryParams, setModalView, setQueryParams]);
|
||||||
|
|
||||||
const { localFile } = useMountMediaStore();
|
|
||||||
useEffect(() => {
|
|
||||||
if (!diskChannel || !localFile) return;
|
|
||||||
diskChannel.onmessage = async e => {
|
|
||||||
console.debug("Received", e.data);
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
const blob = localFile.slice(data.start, data.end);
|
|
||||||
const buf = await blob.arrayBuffer();
|
|
||||||
const header = new ArrayBuffer(16);
|
|
||||||
const headerView = new DataView(header);
|
|
||||||
headerView.setBigUint64(0, BigInt(data.start), false); // start offset, big-endian
|
|
||||||
headerView.setBigUint64(8, BigInt(buf.byteLength), false); // length, big-endian
|
|
||||||
const fullData = new Uint8Array(header.byteLength + buf.byteLength);
|
|
||||||
fullData.set(new Uint8Array(header), 0);
|
|
||||||
fullData.set(new Uint8Array(buf), header.byteLength);
|
|
||||||
diskChannel.send(fullData);
|
|
||||||
};
|
|
||||||
}, [diskChannel, localFile]);
|
|
||||||
|
|
||||||
// System update
|
// System update
|
||||||
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
||||||
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
||||||
|
|
|
@ -69,11 +69,6 @@ func setMassStorageMode(cdrom bool) error {
|
||||||
return gadget.UpdateGadgetConfig()
|
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 {
|
func mountImage(imagePath string) error {
|
||||||
err := setMassStorageImage("")
|
err := setMassStorageImage("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -166,7 +161,6 @@ func rpcCheckMountUrl(url string) (*VirtualMediaUrlInfo, error) {
|
||||||
type VirtualMediaSource string
|
type VirtualMediaSource string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
WebRTC VirtualMediaSource = "WebRTC"
|
|
||||||
HTTP VirtualMediaSource = "HTTP"
|
HTTP VirtualMediaSource = "HTTP"
|
||||||
Storage VirtualMediaSource = "Storage"
|
Storage VirtualMediaSource = "Storage"
|
||||||
)
|
)
|
||||||
|
@ -234,7 +228,6 @@ func getInitialVirtualMediaState() (*VirtualMediaState, error) {
|
||||||
initialState.Mode = CDROM
|
initialState.Mode = CDROM
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: check if it's WebRTC or HTTP
|
|
||||||
switch diskPath {
|
switch diskPath {
|
||||||
case "":
|
case "":
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -313,43 +306,6 @@ func rpcMountWithHTTP(url string, mode VirtualMediaMode) error {
|
||||||
return nil
|
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 {
|
func rpcMountWithStorage(filename string, mode VirtualMediaMode) error {
|
||||||
filename, err := sanitizeFilename(filename)
|
filename, err := sanitizeFilename(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -21,7 +21,6 @@ type Session struct {
|
||||||
ControlChannel *webrtc.DataChannel
|
ControlChannel *webrtc.DataChannel
|
||||||
RPCChannel *webrtc.DataChannel
|
RPCChannel *webrtc.DataChannel
|
||||||
HidChannel *webrtc.DataChannel
|
HidChannel *webrtc.DataChannel
|
||||||
DiskChannel *webrtc.DataChannel
|
|
||||||
shouldUmountVirtualMedia bool
|
shouldUmountVirtualMedia bool
|
||||||
rpcQueue chan webrtc.DataChannelMessage
|
rpcQueue chan webrtc.DataChannelMessage
|
||||||
}
|
}
|
||||||
|
@ -126,9 +125,6 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
triggerVideoStateUpdate()
|
triggerVideoStateUpdate()
|
||||||
triggerUSBStateUpdate()
|
triggerUSBStateUpdate()
|
||||||
case "disk":
|
|
||||||
session.DiskChannel = d
|
|
||||||
d.OnMessage(onDiskMessage)
|
|
||||||
case "terminal":
|
case "terminal":
|
||||||
handleTerminalChannel(d)
|
handleTerminalChannel(d)
|
||||||
case "serial":
|
case "serial":
|
||||||
|
|
Loading…
Reference in New Issue