mirror of https://github.com/jetkvm/kvm.git
Compare commits
7 Commits
59e86e9cf9
...
a614b8bbe4
Author | SHA1 | Date |
---|---|---|
|
a614b8bbe4 | |
|
2a99c2db9d | |
|
0b5033f798 | |
|
d07bedb323 | |
|
aa0f38bc0b | |
|
a819739790 | |
|
7602aefe98 |
|
@ -22,6 +22,7 @@ type Config struct {
|
||||||
LocalAuthToken string `json:"local_auth_token"`
|
LocalAuthToken string `json:"local_auth_token"`
|
||||||
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
||||||
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
||||||
|
EdidString string `json:"hdmi_edid_string"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const configPath = "/userdata/kvm_config.json"
|
const configPath = "/userdata/kvm_config.json"
|
||||||
|
|
|
@ -183,6 +183,12 @@ func rpcSetEDID(edid string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save EDID to config, allowing it to be restored on reboot.
|
||||||
|
LoadConfig()
|
||||||
|
config.EdidString = edid
|
||||||
|
SaveConfig()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
native.go
16
native.go
|
@ -152,6 +152,9 @@ func handleCtrlClient(conn net.Conn) {
|
||||||
|
|
||||||
ctrlSocketConn = conn
|
ctrlSocketConn = conn
|
||||||
|
|
||||||
|
// Restore HDMI EDID if applicable
|
||||||
|
go restoreHdmiEdid()
|
||||||
|
|
||||||
readBuf := make([]byte, 4096)
|
readBuf := make([]byte, 4096)
|
||||||
for {
|
for {
|
||||||
n, err := conn.Read(readBuf)
|
n, err := conn.Read(readBuf)
|
||||||
|
@ -304,3 +307,16 @@ func ensureBinaryUpdated(destPath string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore the HDMI EDID value from the config.
|
||||||
|
// Called after successful connection to jetkvm_native.
|
||||||
|
func restoreHdmiEdid() {
|
||||||
|
LoadConfig()
|
||||||
|
if config.EdidString != "" {
|
||||||
|
logger.Infof("Restoring HDMI EDID to %v", config.EdidString)
|
||||||
|
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to restore HDMI EDID: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
35
network.go
35
network.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"golang.org/x/net/ipv4"
|
"golang.org/x/net/ipv4"
|
||||||
"golang.org/x/net/ipv6"
|
"golang.org/x/net/ipv6"
|
||||||
"net"
|
"net"
|
||||||
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
|
@ -25,6 +26,23 @@ type LocalIpInfo struct {
|
||||||
MAC string
|
MAC string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setDhcpClientState sends signals to udhcpc to change it's current mode
|
||||||
|
// of operation. Setting active to true will force udhcpc to renew the DHCP lease.
|
||||||
|
// Setting active to false will put udhcpc into idle mode.
|
||||||
|
func setDhcpClientState(active bool) {
|
||||||
|
var signal string;
|
||||||
|
if active {
|
||||||
|
signal = "-SIGUSR1"
|
||||||
|
} else {
|
||||||
|
signal = "-SIGUSR2"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("/usr/bin/killall", signal, "udhcpc");
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf("network: setDhcpClientState: failed to change udhcpc state: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func checkNetworkState() {
|
func checkNetworkState() {
|
||||||
iface, err := netlink.LinkByName("eth0")
|
iface, err := netlink.LinkByName("eth0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -47,9 +65,26 @@ func checkNetworkState() {
|
||||||
fmt.Printf("failed to get addresses for eth0: %v\n", err)
|
fmt.Printf("failed to get addresses for eth0: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the link is going down, put udhcpc into idle mode.
|
||||||
|
// If the link is coming back up, activate udhcpc and force it to renew the lease.
|
||||||
|
if newState.Up != networkState.Up {
|
||||||
|
setDhcpClientState(newState.Up)
|
||||||
|
}
|
||||||
|
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
if addr.IP.To4() != nil {
|
if addr.IP.To4() != nil {
|
||||||
|
if !newState.Up && networkState.Up {
|
||||||
|
// If the network is going down, remove all IPv4 addresses from the interface.
|
||||||
|
fmt.Printf("network: state transitioned to down, removing IPv4 address %s\n", addr.IP.String())
|
||||||
|
err := netlink.AddrDel(iface, &addr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("network: failed to delete %s", addr.IP.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
newState.IPv4 = "..."
|
||||||
|
} else {
|
||||||
newState.IPv4 = addr.IP.String()
|
newState.IPv4 = addr.IP.String()
|
||||||
|
}
|
||||||
} else if addr.IP.To16() != nil && newState.IPv6 == "" {
|
} else if addr.IP.To16() != nil && newState.IPv6 == "" {
|
||||||
newState.IPv6 = addr.IP.String()
|
newState.IPv6 = addr.IP.String()
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,14 @@ import Container from "@components/Container";
|
||||||
import { LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
|
import { LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import PasteModal from "@/components/popovers/PasteModal";
|
import PasteModal from "@/components/popovers/PasteModal";
|
||||||
import { FaKeyboard } from "react-icons/fa6";
|
import { FaKeyboard, FaLock } from "react-icons/fa6";
|
||||||
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
|
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
|
||||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||||
import MountPopopover from "./popovers/MountPopover";
|
import MountPopopover from "./popovers/MountPopover";
|
||||||
import { Fragment, useCallback, useRef } from "react";
|
import { Fragment, useCallback, useRef } from "react";
|
||||||
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
||||||
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export default function Actionbar({
|
export default function Actionbar({
|
||||||
requestFullscreen,
|
requestFullscreen,
|
||||||
|
@ -54,6 +56,8 @@ export default function Actionbar({
|
||||||
[setDisableFocusTrap],
|
[setDisableFocusTrap],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="bg-white border-b border-b-slate-800/20 dark:bg-slate-900 dark:border-b-slate-300/20">
|
<Container className="bg-white border-b border-b-slate-800/20 dark:bg-slate-900 dark:border-b-slate-300/20">
|
||||||
<div
|
<div
|
||||||
|
@ -205,6 +209,23 @@ export default function Actionbar({
|
||||||
onClick={() => setVirtualKeyboard(!virtualKeyboard)}
|
onClick={() => setVirtualKeyboard(!virtualKeyboard)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{useSettingsStore().actionBarCtrlAltDel && (
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<Button
|
||||||
|
size="XS"
|
||||||
|
theme="light"
|
||||||
|
text="Ctrl + Alt + Del"
|
||||||
|
LeadingIcon={FaLock}
|
||||||
|
onClick={() => {
|
||||||
|
sendKeyboardEvent(
|
||||||
|
[keys["Delete"]],
|
||||||
|
[modifiers["ControlLeft"], modifiers["AltLeft"]],
|
||||||
|
);
|
||||||
|
setTimeout(resetKeyboardState, 100);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-2">
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-2">
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { InputFieldWithLabel } from "./InputField";
|
||||||
import DebianIcon from "@/assets/debian-icon.png";
|
import DebianIcon from "@/assets/debian-icon.png";
|
||||||
import UbuntuIcon from "@/assets/ubuntu-icon.png";
|
import UbuntuIcon from "@/assets/ubuntu-icon.png";
|
||||||
import FedoraIcon from "@/assets/fedora-icon.png";
|
import FedoraIcon from "@/assets/fedora-icon.png";
|
||||||
|
import OpenSUSEIcon from "@/assets/opensuse-icon.png";
|
||||||
import ArchIcon from "@/assets/arch-icon.png";
|
import ArchIcon from "@/assets/arch-icon.png";
|
||||||
import NetBootIcon from "@/assets/netboot-icon.svg";
|
import NetBootIcon from "@/assets/netboot-icon.svg";
|
||||||
import { TrashIcon } from "@heroicons/react/16/solid";
|
import { TrashIcon } from "@heroicons/react/16/solid";
|
||||||
|
@ -542,6 +543,16 @@ function UrlView({
|
||||||
url: "https://download.fedoraproject.org/pub/fedora/linux/releases/41/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-41-1.4.iso",
|
url: "https://download.fedoraproject.org/pub/fedora/linux/releases/41/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-41-1.4.iso",
|
||||||
icon: FedoraIcon,
|
icon: FedoraIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "openSUSE Leap 15.6",
|
||||||
|
url: "https://download.opensuse.org/distribution/leap/15.6/iso/openSUSE-Leap-15.6-NET-x86_64-Media.iso",
|
||||||
|
icon: OpenSUSEIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "openSUSE Tumbleweed",
|
||||||
|
url: "https://download.opensuse.org/tumbleweed/iso/openSUSE-Tumbleweed-NET-x86_64-Current.iso",
|
||||||
|
icon: OpenSUSEIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Arch Linux",
|
name: "Arch Linux",
|
||||||
url: "https://archlinux.doridian.net/iso/2025.02.01/archlinux-2025.02.01-x86_64.iso",
|
url: "https://archlinux.doridian.net/iso/2025.02.01/archlinux-2025.02.01-x86_64.iso",
|
||||||
|
|
|
@ -466,7 +466,7 @@ export default function SettingsSidebar() {
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="flex items-center px-4 py-3 group gap-x-4">
|
<div className="flex items-center px-4 py-3 group gap-x-4">
|
||||||
<img
|
<img
|
||||||
className="w-6 shrink-0"
|
className="w-6 shrink-0 dark:invert"
|
||||||
src={PointingFinger}
|
src={PointingFinger}
|
||||||
alt="Finger touching a screen"
|
alt="Finger touching a screen"
|
||||||
/>
|
/>
|
||||||
|
@ -490,7 +490,7 @@ export default function SettingsSidebar() {
|
||||||
>
|
>
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="flex items-center px-4 py-3 gap-x-4">
|
<div className="flex items-center px-4 py-3 gap-x-4">
|
||||||
<img className="w-6 shrink-0" src={MouseIcon} alt="Mouse icon" />
|
<img className="w-6 shrink-0 dark:invert" src={MouseIcon} alt="Mouse icon" />
|
||||||
<div className="flex items-center justify-between grow">
|
<div className="flex items-center justify-between grow">
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<h3 className="text-sm font-semibold text-black dark:text-white">
|
<h3 className="text-sm font-semibold text-black dark:text-white">
|
||||||
|
@ -796,6 +796,15 @@ export default function SettingsSidebar() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
<SettingsItem
|
||||||
|
title="Ctrl + Alt + Del Button"
|
||||||
|
description="Display Ctrl + Alt + Del button on the Action Bar"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={settings.actionBarCtrlAltDel}
|
||||||
|
onChange={e => settings.setActionBarCtrlAltDel(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-2 space-y-4">
|
<div className="pb-2 space-y-4">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
|
|
|
@ -270,6 +270,9 @@ interface SettingsState {
|
||||||
// Add new developer mode state
|
// Add new developer mode state
|
||||||
developerMode: boolean;
|
developerMode: boolean;
|
||||||
setDeveloperMode: (enabled: boolean) => void;
|
setDeveloperMode: (enabled: boolean) => void;
|
||||||
|
|
||||||
|
actionBarCtrlAltDel: boolean;
|
||||||
|
setActionBarCtrlAltDel: (enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = create(
|
export const useSettingsStore = create(
|
||||||
|
@ -287,6 +290,9 @@ export const useSettingsStore = create(
|
||||||
// Add developer mode with default value
|
// Add developer mode with default value
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
||||||
|
|
||||||
|
actionBarCtrlAltDel: false,
|
||||||
|
setActionBarCtrlAltDel: enabled => set({ actionBarCtrlAltDel: enabled }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
|
|
Loading…
Reference in New Issue