Compare commits

...

5 Commits

Author SHA1 Message Date
Ben Kochie def37141c8
Merge dd1189e8ad into 63b3ef0151 2025-02-12 16:42:24 +01:00
Andrew Nicholson 63b3ef0151
Enable "Boot Interface Subclass" for keyboard and mouse. (#113)
This is often required for the keyboard/mouse to be recognized in
BIOS/UEFI firmware.
2025-02-12 15:08:03 +01:00
Brandon Tuttle 69168ff062
Fix fullscreen video relative mouse movements (#85) 2025-02-11 20:00:50 +01:00
SuperQ dd1189e8ad
Chore: Enable golangci-lint
Add a GitHub actions workflow to run golangci-lint.

Signed-off-by: SuperQ <superq@gmail.com>
2025-01-05 18:36:25 +01:00
SuperQ c380c702c7
Chore: Fix up various linting issues
In prep to add golangci-lint, fix various linting issues.
* Make the `kvm` package a fully-qualified public package.

Signed-off-by: SuperQ <superq@gmail.com>
2025-01-05 18:35:47 +01:00
19 changed files with 135 additions and 40 deletions

34
.github/workflows/golangci-lint.yml vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: golangci-lint
on:
push:
paths:
- "go.sum"
- "go.mod"
- "**.go"
- ".github/workflows/golangci-lint.yml"
- ".golangci.yml"
pull_request:
permissions: # added using https://github.com/step-security/secure-repo
contents: read
jobs:
golangci:
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Go
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: 1.23.x
- name: Lint
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
with:
args: --verbose
version: v1.62.0

12
.golangci.yml Normal file
View File

@ -0,0 +1,12 @@
---
linters:
enable:
# - goimports
# - misspell
# - revive
issues:
exclude-rules:
- path: _test.go
linters:
- errcheck

View File

@ -130,7 +130,7 @@ func runWebsocketClient() error {
if err != nil {
return err
}
defer c.CloseNow()
defer c.CloseNow() //nolint:errcheck
logger.Infof("WS connected to %v", wsURL.String())
runCtx, cancelRun := context.WithCancel(context.Background())
defer cancelRun()

View File

@ -1,7 +1,7 @@
package main
import (
"kvm"
"github.com/jetkvm/kvm"
)
func main() {

2
go.mod
View File

@ -1,4 +1,4 @@
module kvm
module github.com/jetkvm/kvm
go 1.21.0

4
hw.go
View File

@ -14,7 +14,7 @@ func extractSerialNumber() (string, error) {
return "", err
}
r, err := regexp.Compile("Serial\\s*:\\s*(\\S+)")
r, err := regexp.Compile(`Serial\s*:\s*(\S+)`)
if err != nil {
return "", fmt.Errorf("failed to compile regex: %w", err)
}
@ -27,7 +27,7 @@ func extractSerialNumber() (string, error) {
return matches[1], nil
}
func readOtpEntropy() ([]byte, error) {
func readOtpEntropy() ([]byte, error) { //nolint:unused
content, err := os.ReadFile("/sys/bus/nvmem/devices/rockchip-otp0/nvmem")
if err != nil {
return nil, err

View File

@ -48,7 +48,7 @@ func Main() {
time.Sleep(15 * time.Minute)
for {
logger.Debugf("UPDATING - Auto update enabled: %v", config.AutoUpdateEnabled)
if config.AutoUpdateEnabled == false {
if !config.AutoUpdateEnabled {
return
}
if currentSession != nil {

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"io"
"kvm/resource"
"log"
"net"
"os"
@ -14,6 +13,8 @@ import (
"syscall"
"time"
"github.com/jetkvm/kvm/resource"
"github.com/pion/webrtc/v4/pkg/media"
)
@ -91,8 +92,8 @@ func WriteCtrlMessage(message []byte) error {
return err
}
var nativeCtrlSocketListener net.Listener
var nativeVideoSocketListener net.Listener
var nativeCtrlSocketListener net.Listener //nolint:unused
var nativeVideoSocketListener net.Listener //nolint:unused
var ctrlClientConnected = make(chan struct{})
@ -192,7 +193,7 @@ func handleVideoClient(conn net.Conn) {
for {
n, err := conn.Read(inboundPacket)
if err != nil {
log.Println("error during read: %s", err)
log.Printf("error during read: %s\n", err)
return
}
now := time.Now()

View File

@ -28,7 +28,7 @@ type LocalIpInfo struct {
func checkNetworkState() {
iface, err := netlink.LinkByName("eth0")
if err != nil {
fmt.Printf("failed to get eth0 interface: %v\n", err)
fmt.Printf("failed to get eth0 interface: %s\n", err)
return
}
@ -44,7 +44,7 @@ func checkNetworkState() {
addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL)
if err != nil {
fmt.Printf("failed to get addresses for eth0: %v\n", err)
fmt.Printf("failed to get addresses for eth0: %s\n", err)
}
for _, addr := range addrs {
@ -98,7 +98,7 @@ func init() {
done := make(chan struct{})
if err := netlink.LinkSubscribe(updates, done); err != nil {
fmt.Println("failed to subscribe to link updates: %v", err)
fmt.Printf("failed to subscribe to link updates: %v\n", err)
return
}
@ -125,6 +125,6 @@ func init() {
fmt.Println("Starting mDNS server")
err := startMDNS()
if err != nil {
fmt.Println("failed to run mDNS: %v", err)
fmt.Printf("failed to run mDNS: %v\n", err)
}
}

2
ntp.go
View File

@ -11,7 +11,7 @@ import (
"github.com/beevik/ntp"
)
var timeSynced = false
var timeSynced = false //nolint:unused
func TimeSyncLoop() {
for {

View File

@ -49,7 +49,7 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) (
if err != nil {
return nil, err
}
buf := make([]byte, 0)
var buf []byte
for {
select {
case data := <-diskReadChan:

View File

@ -55,11 +55,13 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
var size TerminalSize
err := json.Unmarshal([]byte(msg.Data), &size)
if err == nil {
pty.Setsize(ptmx, &pty.Winsize{
err = pty.Setsize(ptmx, &pty.Winsize{
Rows: uint16(size.Rows),
Cols: uint16(size.Cols),
})
return
if err == nil {
return
}
}
logger.Errorf("Failed to parse terminal size: %v", err)
}
@ -74,7 +76,7 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
ptmx.Close()
}
if cmd != nil && cmd.Process != nil {
cmd.Process.Kill()
_ = cmd.Process.Kill()
}
})
}

View File

@ -10,6 +10,7 @@
"dev": "vite dev --mode=development",
"build": "npm run build:prod",
"build:device": "tsc && vite build --mode=device --emptyOutDir",
"dev:device": "vite dev --mode=device",
"build:prod": "tsc && vite build --mode=production",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},

View File

@ -4,6 +4,7 @@ import {
useMountMediaStore,
useUiStore,
useSettingsStore,
useVideoStore,
} from "@/hooks/stores";
import { MdOutlineContentPasteGo } from "react-icons/md";
import Container from "@components/Container";
@ -33,6 +34,7 @@ export default function Actionbar({
state => state.remoteVirtualMediaState,
);
const developerMode = useSettingsStore(state => state.developerMode);
const hdmiState = useVideoStore(state => state.hdmiState);
// This is the only way to get a reliable state change for the popover
// at time of writing this there is no mount, or unmount event for the popover
@ -247,6 +249,7 @@ export default function Actionbar({
size="XS"
theme="light"
text="Fullscreen"
disabled={hdmiState !== 'ready'}
LeadingIcon={LuMaximize}
onClick={() => requestFullscreen()}
/>

View File

@ -30,6 +30,8 @@ export default function WebRTCVideo() {
const {
setClientSize: setVideoClientSize,
setSize: setVideoSize,
width: videoWidth,
height: videoHeight,
clientWidth: videoClientWidth,
clientHeight: videoClientHeight,
} = useVideoStore();
@ -102,20 +104,43 @@ export default function WebRTCVideo() {
const mouseMoveHandler = useCallback(
(e: MouseEvent) => {
if (!videoClientWidth || !videoClientHeight) return;
const { buttons } = e;
// Get the aspect ratios of the video element and the video stream
const videoElementAspectRatio = videoClientWidth / videoClientHeight;
const videoStreamAspectRatio = videoWidth / videoHeight;
// Clamp mouse position within the video boundaries
const currMouseX = Math.min(Math.max(1, e.offsetX), videoClientWidth);
const currMouseY = Math.min(Math.max(1, e.offsetY), videoClientHeight);
// Calculate the effective video display area
let effectiveWidth = videoClientWidth;
let effectiveHeight = videoClientHeight;
let offsetX = 0;
let offsetY = 0;
// Normalize mouse position to 0-32767 range (HID absolute coordinate system)
const x = Math.round((currMouseX / videoClientWidth) * 32767);
const y = Math.round((currMouseY / videoClientHeight) * 32767);
if (videoElementAspectRatio > videoStreamAspectRatio) {
// Pillarboxing: black bars on the left and right
effectiveWidth = videoClientHeight * videoStreamAspectRatio;
offsetX = (videoClientWidth - effectiveWidth) / 2;
} else if (videoElementAspectRatio < videoStreamAspectRatio) {
// Letterboxing: black bars on the top and bottom
effectiveHeight = videoClientWidth / videoStreamAspectRatio;
offsetY = (videoClientHeight - effectiveHeight) / 2;
}
// Clamp mouse position within the effective video boundaries
const clampedX = Math.min(Math.max(offsetX, e.offsetX), offsetX + effectiveWidth);
const clampedY = Math.min(Math.max(offsetY, e.offsetY), offsetY + effectiveHeight);
// Map clamped mouse position to the video stream's coordinate system
const relativeX = (clampedX - offsetX) / effectiveWidth;
const relativeY = (clampedY - offsetY) / effectiveHeight;
// Convert to HID absolute coordinate system (0-32767 range)
const x = Math.round(relativeX * 32767);
const y = Math.round(relativeY * 32767);
// Send mouse movement
const { buttons } = e;
sendMouseMovement(x, y, buttons);
},
[sendMouseMovement, videoClientHeight, videoClientWidth],
[sendMouseMovement, videoClientHeight, videoClientWidth, videoWidth, videoHeight],
);
const mouseWheelHandler = useCallback(

View File

@ -2,13 +2,31 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig(({ mode }) => {
declare const process: {
env: {
JETKVM_PROXY_URL: string;
};
};
export default defineConfig(({ mode, command }) => {
const isCloud = mode === "production";
const onDevice = mode === "device";
const { JETKVM_PROXY_URL } = process.env;
return {
plugins: [tsconfigPaths(), react()],
build: { outDir: isCloud ? "dist" : "../static" },
server: { host: "0.0.0.0" },
base: onDevice ? "/static" : "/",
server: {
host: "0.0.0.0",
proxy: JETKVM_PROXY_URL ? {
'/me': JETKVM_PROXY_URL,
'/device': JETKVM_PROXY_URL,
'/webrtc': JETKVM_PROXY_URL,
'/auth': JETKVM_PROXY_URL,
'/storage': JETKVM_PROXY_URL,
'/cloud': JETKVM_PROXY_URL,
} : undefined
},
base: onDevice && command === 'build' ? "/static" : "/",
};
});

6
usb.go
View File

@ -132,7 +132,7 @@ func writeGadgetConfig() error {
}
err = writeGadgetAttrs(hid0Path, [][]string{
{"protocol", "1"},
{"subclass", "0"},
{"subclass", "1"},
{"report_length", "8"},
})
if err != nil {
@ -152,7 +152,7 @@ func writeGadgetConfig() error {
}
err = writeGadgetAttrs(hid1Path, [][]string{
{"protocol", "2"},
{"subclass", "0"},
{"subclass", "1"},
{"report_length", "6"},
})
if err != nil {
@ -215,7 +215,7 @@ func writeGadgetConfig() error {
return nil
}
func rebindUsb() error {
func rebindUsb() error { //nolint:unused
err := os.WriteFile("/sys/bus/platform/drivers/dwc3/unbind", []byte(udc), 0644)
if err != nil {
return err

View File

@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"io"
"kvm/resource"
"log"
"net/http"
"os"
@ -16,12 +15,12 @@ import (
"syscall"
"time"
"github.com/jetkvm/kvm/resource"
"github.com/gin-gonic/gin"
"github.com/psanford/httpreadat"
"github.com/google/uuid"
"github.com/pion/webrtc/v4"
"github.com/psanford/httpreadat"
)
const massStorageName = "mass_storage.usb0"
@ -60,11 +59,11 @@ func onDiskMessage(msg webrtc.DataChannelMessage) {
func mountImage(imagePath string) error {
err := setMassStorageImage("")
if err != nil {
return fmt.Errorf("Remove Mass Storage Image Error", err)
return fmt.Errorf("Remove Mass Storage Image Error: %w", err)
}
err = setMassStorageImage(imagePath)
if err != nil {
return fmt.Errorf("Set Mass Storage Image Error", err)
return fmt.Errorf("Set Mass Storage Image Error: %w", err)
}
return nil
}

2
wol.go
View File

@ -43,7 +43,7 @@ func createMagicPacket(mac net.HardwareAddr) []byte {
// Write the target MAC address 16 times
for i := 0; i < 16; i++ {
binary.Write(&buf, binary.BigEndian, mac)
_ = binary.Write(&buf, binary.BigEndian, mac)
}
return buf.Bytes()