Compare commits

...

7 Commits

Author SHA1 Message Date
Cameron Fleming a614b8bbe4
Merge a819739790 into 2a99c2db9d 2025-02-13 15:41:28 +01:00
Cameron Fleming 2a99c2db9d
fix(net): stop dhcp client and release all v4 addr on linkdown (#16)
This commit fixes jetkvm/kvm#12 by disabling the udhcpc client when the
link goes down, it then removes all the active IPv4 addresses from the
deivce.

Once the link comes back up, it re-activates the udhcpc client so it can
fetch a new IPv4 address for the device.

This doesn't make any changes to the IPv6 side of things yet.
2025-02-13 15:41:10 +01:00
Cameron Fleming 0b5033f798
feat: restore EDID on reboot (#34)
This commit adds the config entry "EdidString" and saves the EDID string
when it's modified via the RPC.

The EDID is restored when the jetkvm_native control socket connects
(usually at boot)

Signed-off-by: Cameron Fleming <cameron@nevexo.space>
2025-02-13 14:42:07 +01:00
Scai d07bedb323
Invert colors on Icons (#123)
* feat(ui): invert colors on icons

* feat(ui): fix tailwindcss class for invert
2025-02-13 14:33:03 +01:00
Dominik Heidler aa0f38bc0b
Add openSUSE ISOs (#151) 2025-02-13 14:05:07 +01:00
Cameron Fleming a819739790 feat(ui): make Ctrl + Alt + Del button a setting
This commit makes the Action Bar Ctrl + Alt + Del button a setting,
which is off by default.
2025-01-04 00:22:08 +00:00
Cameron Fleming 7602aefe98 feat(ui/ActionBar): add Ctrl + Alt + Del button to Action Bar
This commit adds a CTRL + ALT + DEL button to the Action Bar allowing
you to send the combination to the target machine without launching the
Virtual Keyboard, or sending the signal to the computer you're accessing
the KVM from. This is useful for people installing OS', or potentially
debugging kernel issues.
2025-01-04 00:10:56 +00:00
8 changed files with 109 additions and 4 deletions

View File

@ -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"

View File

@ -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
} }

View File

@ -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)
}
}
}

View File

@ -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()
} }

View File

@ -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">

View File

@ -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",

View File

@ -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

View File

@ -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",