mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-01-18 03:28:19 +01:00
Update App version to 0.0.4
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
@@ -345,15 +345,35 @@ func (f *FieldConfig) validateField() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
val, err := toString(f.CurrentValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("field `%s` cannot use validate_type: %s", f.Name, err)
|
||||
switch v := f.CurrentValue.(type) {
|
||||
case []string:
|
||||
// Validate each element in the slice
|
||||
for _, item := range v {
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if err := f.validateScalarValue(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
val, err := toString(f.CurrentValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("field `%s` cannot use validate_type: %s", f.Name, err)
|
||||
}
|
||||
if val == "" {
|
||||
return nil
|
||||
}
|
||||
if err := f.validateScalarValue(val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateScalarValue applies ValidateTypes to a single string value.
|
||||
func (f *FieldConfig) validateScalarValue(val string) error {
|
||||
for _, validateType := range f.ValidateTypes {
|
||||
switch validateType {
|
||||
case "ipv4":
|
||||
@@ -376,6 +396,5 @@ func (f *FieldConfig) validateField() error {
|
||||
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,6 +22,22 @@ func toString(v interface{}) (string, error) {
|
||||
return v, nil
|
||||
case null.String:
|
||||
return v.String, nil
|
||||
case []string:
|
||||
if len(v) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
if len(v) == 1 {
|
||||
return v[0], nil
|
||||
}
|
||||
return strings.Join(v, ","), nil
|
||||
case []interface{}:
|
||||
if len(v) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
if s, ok := v[0].(string); ok {
|
||||
return s, nil
|
||||
}
|
||||
return "", fmt.Errorf("unsupported type in slice: %T", v[0])
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unsupported type: %s", reflect.TypeOf(v))
|
||||
|
||||
@@ -33,23 +33,24 @@ type IPv6StaticConfig struct {
|
||||
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
|
||||
}
|
||||
type NetworkConfig struct {
|
||||
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
||||
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
|
||||
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
||||
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
|
||||
|
||||
IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"`
|
||||
IPv4RequestAddress null.String `json:"ipv4_request_address,omitempty"`
|
||||
IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"`
|
||||
IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"`
|
||||
IPv4RequestAddress null.String `json:"ipv4_request_address,omitempty"`
|
||||
IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"`
|
||||
|
||||
IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
||||
IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
||||
IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
||||
IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
||||
|
||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
|
||||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
|
||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
|
||||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
|
||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
||||
PendingReboot null.Bool `json:"pending_reboot,omitempty" default:"false"`
|
||||
}
|
||||
|
||||
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"kvm/internal/confparser"
|
||||
"kvm/internal/logging"
|
||||
"kvm/internal/udhcpc"
|
||||
"kvm/internal/confparser"
|
||||
"kvm/internal/logging"
|
||||
"kvm/internal/udhcpc"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
@@ -100,6 +103,31 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
||||
|
||||
s.dhcpClient = dhcpClient
|
||||
|
||||
mode := strings.TrimSpace(s.config.IPv4Mode.String)
|
||||
switch mode {
|
||||
case "static":
|
||||
if s.dhcpClient != nil {
|
||||
s.dhcpClient.SetEnabled(false)
|
||||
}
|
||||
if err := s.applyIPv4Static(); err != nil {
|
||||
l.Error().Err(err).Msg("failed to apply static IPv4 on init")
|
||||
}
|
||||
case "dhcp":
|
||||
if s.dhcpClient != nil {
|
||||
s.dhcpClient.SetEnabled(true)
|
||||
}
|
||||
case "disabled":
|
||||
if s.dhcpClient != nil {
|
||||
s.dhcpClient.SetEnabled(false)
|
||||
}
|
||||
if err := s.clearIPv4Addresses(); err != nil {
|
||||
l.Warn().Err(err).Msg("failed to clear IPv4 addresses on init")
|
||||
}
|
||||
if err := s.clearDefaultIPv4Route(); err != nil {
|
||||
l.Debug().Err(err).Msg("failed to clear default route on init")
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -343,6 +371,162 @@ func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) onConfigChange(config *NetworkConfig) {
|
||||
_ = s.setHostnameIfNotSame()
|
||||
s.cbConfigChange(config)
|
||||
_ = s.setHostnameIfNotSame()
|
||||
s.cbConfigChange(config)
|
||||
}
|
||||
|
||||
// clearIPv4Addresses removes all IPv4 addresses from the interface.
|
||||
func (s *NetworkInterfaceState) clearIPv4Addresses() error {
|
||||
iface, err := netlink.LinkByName(s.interfaceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addrs, err := netlinkAddrs(iface)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if addr.IP.To4() != nil {
|
||||
if err := netlink.AddrDel(iface, &addr); err != nil {
|
||||
s.l.Warn().Err(err).Str("addr", addr.IPNet.String()).Msg("failed to delete IPv4 address")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearDefaultIPv4Route removes existing default IPv4 route on this interface.
|
||||
func (s *NetworkInterfaceState) clearDefaultIPv4Route() error {
|
||||
iface, err := netlink.LinkByName(s.interfaceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
routes, err := netlink.RouteList(iface, netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range routes {
|
||||
if r.Dst == nil { // default route
|
||||
if err := netlink.RouteDel(&r); err != nil {
|
||||
s.l.Warn().Err(err).Msg("failed to delete default route")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseIPv4Mask converts dotted decimal netmask to net.IPMask.
|
||||
func parseIPv4Mask(mask string) (net.IPMask, error) {
|
||||
parts := strings.Split(strings.TrimSpace(mask), ".")
|
||||
if len(parts) != 4 {
|
||||
return nil, fmt.Errorf("invalid netmask: %s", mask)
|
||||
}
|
||||
bytes := make([]byte, 4)
|
||||
for i := 0; i < 4; i++ {
|
||||
v, err := strconv.Atoi(parts[i])
|
||||
if err != nil || v < 0 || v > 255 {
|
||||
return nil, fmt.Errorf("invalid netmask octet: %s", parts[i])
|
||||
}
|
||||
bytes[i] = byte(v)
|
||||
}
|
||||
return net.IPv4Mask(bytes[0], bytes[1], bytes[2], bytes[3]), nil
|
||||
}
|
||||
|
||||
// writeResolvConf writes DNS servers and optional domain into /etc/resolv.conf.
|
||||
func (s *NetworkInterfaceState) writeResolvConf(dns []string, domain string) error {
|
||||
var b strings.Builder
|
||||
if domain != "" {
|
||||
b.WriteString("search ")
|
||||
b.WriteString(domain)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
for _, d := range dns {
|
||||
d = strings.TrimSpace(d)
|
||||
if d == "" {
|
||||
continue
|
||||
}
|
||||
b.WriteString("nameserver ")
|
||||
b.WriteString(d)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
content := b.String()
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
if err := os.WriteFile("/etc/resolv.conf", []byte(content), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
s.l.Info().Msg("updated /etc/resolv.conf for static IPv4")
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyIPv4Static sets a static IPv4 address, default route, and DNS.
|
||||
func (s *NetworkInterfaceState) applyIPv4Static() error {
|
||||
if s.config == nil || s.config.IPv4Static == nil {
|
||||
return fmt.Errorf("IPv4Static config not provided")
|
||||
}
|
||||
ipStr := strings.TrimSpace(s.config.IPv4Static.Address.String)
|
||||
maskStr := strings.TrimSpace(s.config.IPv4Static.Netmask.String)
|
||||
gwStr := strings.TrimSpace(s.config.IPv4Static.Gateway.String)
|
||||
dns := s.config.IPv4Static.DNS
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil || ip.To4() == nil {
|
||||
return fmt.Errorf("invalid IPv4 address: %s", ipStr)
|
||||
}
|
||||
mask, err := parseIPv4Mask(maskStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
iface, err := netlink.LinkByName(s.interfaceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear existing IPv4 addresses and default route
|
||||
if err := s.clearIPv4Addresses(); err != nil {
|
||||
s.l.Warn().Err(err).Msg("failed clearing IPv4 addresses prior to static apply")
|
||||
}
|
||||
if err := s.clearDefaultIPv4Route(); err != nil {
|
||||
s.l.Warn().Err(err).Msg("failed clearing default route prior to static apply")
|
||||
}
|
||||
|
||||
ipNet := &net.IPNet{IP: ip, Mask: mask}
|
||||
addr := &netlink.Addr{IPNet: ipNet}
|
||||
if err := netlink.AddrAdd(iface, addr); err != nil {
|
||||
return logging.ErrorfL(s.l, "failed to add static IPv4 address", err)
|
||||
}
|
||||
s.l.Info().Str("ipv4", ipNet.String()).Msg("static IPv4 address applied")
|
||||
|
||||
// Default route
|
||||
if gwStr != "" {
|
||||
gw := net.ParseIP(gwStr)
|
||||
if gw == nil || gw.To4() == nil {
|
||||
s.l.Warn().Str("gateway", gwStr).Msg("invalid IPv4 gateway; skipping route")
|
||||
} else {
|
||||
route := netlink.Route{LinkIndex: iface.Attrs().Index, Gw: gw}
|
||||
// remove any existing default routes already attempted above, then add
|
||||
if err := netlink.RouteAdd(&route); err != nil {
|
||||
// try replace if add failed
|
||||
if replaceErr := netlink.RouteReplace(&route); replaceErr != nil {
|
||||
s.l.Warn().Err(err).Msg("failed to add default route")
|
||||
}
|
||||
}
|
||||
s.l.Info().Str("gateway", gwStr).Msg("default route applied")
|
||||
}
|
||||
}
|
||||
|
||||
// DNS
|
||||
if len(dns) > 0 {
|
||||
if err := s.writeResolvConf(dns, s.GetDomain()); err != nil {
|
||||
s.l.Warn().Err(err).Msg("failed to write resolv.conf")
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh internal state
|
||||
if _, err := s.update(); err != nil {
|
||||
s.l.Warn().Err(err).Msg("failed to refresh state after static apply")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"kvm/internal/confparser"
|
||||
"kvm/internal/udhcpc"
|
||||
"kvm/internal/confparser"
|
||||
"kvm/internal/udhcpc"
|
||||
|
||||
"github.com/guregu/null/v6"
|
||||
)
|
||||
|
||||
type RpcIPv6Address struct {
|
||||
@@ -99,17 +101,22 @@ func (s *NetworkInterfaceState) RpcGetNetworkSettings() RpcNetworkSettings {
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) RpcSetNetworkSettings(settings RpcNetworkSettings) error {
|
||||
currentSettings := s.config
|
||||
currentSettings := s.config
|
||||
|
||||
err := confparser.SetDefaultsAndValidate(&settings.NetworkConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := confparser.SetDefaultsAndValidate(&settings.NetworkConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if IsSame(currentSettings, settings.NetworkConfig) {
|
||||
// no changes, do nothing
|
||||
return nil
|
||||
}
|
||||
neutralA := *currentSettings
|
||||
neutralB := settings.NetworkConfig
|
||||
neutralA.PendingReboot = null.Bool{}
|
||||
neutralB.PendingReboot = null.Bool{}
|
||||
|
||||
if IsSame(neutralA, neutralB) {
|
||||
// no changes, do nothing
|
||||
return nil
|
||||
}
|
||||
|
||||
s.config = &settings.NetworkConfig
|
||||
s.onConfigChange(s.config)
|
||||
@@ -124,3 +131,10 @@ func (s *NetworkInterfaceState) RpcRenewDHCPLease() error {
|
||||
|
||||
return s.dhcpClient.Renew()
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) RpcRequestDHCPAddress(ip string) error {
|
||||
if s.dhcpClient == nil {
|
||||
return fmt.Errorf("dhcp client not initialized")
|
||||
}
|
||||
return s.dhcpClient.RequestAddress(ip)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ type DHCPClient struct {
|
||||
logger *zerolog.Logger
|
||||
process *os.Process
|
||||
onLeaseChange func(lease *Lease)
|
||||
enabled bool
|
||||
}
|
||||
|
||||
type DHCPClientOptions struct {
|
||||
@@ -57,6 +58,7 @@ func NewDHCPClient(options *DHCPClientOptions) *DHCPClient {
|
||||
pidFile: options.PidFile,
|
||||
onLeaseChange: options.OnLeaseChange,
|
||||
requestAddress: options.RequestAddress,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +89,12 @@ func (c *DHCPClient) watchLink() {
|
||||
for update := range ch {
|
||||
if update.Link.Attrs().Name == c.InterfaceName {
|
||||
if update.Flags&unix.IFF_RUNNING != 0 {
|
||||
c.logger.Info().Msg("link is up, starting udhcpc")
|
||||
go c.runUDHCPC()
|
||||
if c.enabled {
|
||||
c.logger.Info().Msg("link is up, starting udhcpc")
|
||||
go c.runUDHCPC()
|
||||
} else {
|
||||
c.logger.Debug().Msg("link is up, DHCP disabled")
|
||||
}
|
||||
} else {
|
||||
c.logger.Info().Msg("link is down")
|
||||
}
|
||||
@@ -110,6 +116,10 @@ func (w *udhcpcOutput) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (c *DHCPClient) runUDHCPC() {
|
||||
if !c.enabled {
|
||||
c.logger.Debug().Msg("DHCP disabled; skipping udhcpc start")
|
||||
return
|
||||
}
|
||||
cmd := exec.Command("udhcpc", "-i", c.InterfaceName, "-t", "1")
|
||||
if c.requestAddress != "" {
|
||||
ip := net.ParseIP(c.requestAddress)
|
||||
@@ -273,3 +283,29 @@ func (c *DHCPClient) loadLeaseFile() error {
|
||||
func (c *DHCPClient) GetLease() *Lease {
|
||||
return c.lease
|
||||
}
|
||||
|
||||
// RequestAddress updates the requested IPv4 address and restarts udhcpc with -r <ip>.
|
||||
func (c *DHCPClient) RequestAddress(ip string) error {
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed == nil || parsed.To4() == nil {
|
||||
return fmt.Errorf("invalid IPv4 address: %s", ip)
|
||||
}
|
||||
c.requestAddress = ip
|
||||
_ = c.KillProcess()
|
||||
go c.runUDHCPC()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetEnabled toggles DHCP client behavior. When enabling, it will attempt to start udhcpc.
|
||||
// When disabling, it kills any running udhcpc process.
|
||||
func (c *DHCPClient) SetEnabled(enable bool) {
|
||||
if c.enabled == enable {
|
||||
return
|
||||
}
|
||||
c.enabled = enable
|
||||
if enable {
|
||||
go c.runUDHCPC()
|
||||
} else {
|
||||
_ = c.KillProcess()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/procfs"
|
||||
@@ -406,33 +407,79 @@ func (c *ChangeSet) ApplyChanges() error {
|
||||
}
|
||||
|
||||
func (c *ChangeSet) applyChange(change *FileChange) error {
|
||||
// 记录操作详情
|
||||
contentPreview := ""
|
||||
if len(change.ExpectedContent) > 0 && len(change.ExpectedContent) <= 64 {
|
||||
contentPreview = string(change.ExpectedContent)
|
||||
} else if len(change.ExpectedContent) > 64 {
|
||||
contentPreview = string(change.ExpectedContent[:64]) + "..."
|
||||
}
|
||||
|
||||
defaultLogger.Debug().
|
||||
Str("operation", FileChangeResolvedActionString[change.Action()]).
|
||||
Str("path", change.Path).
|
||||
Str("content_preview", contentPreview).
|
||||
Int("content_length", len(change.ExpectedContent)).
|
||||
Msg("executing file operation")
|
||||
|
||||
switch change.Action() {
|
||||
case FileChangeResolvedActionWriteFile:
|
||||
defaultLogger.Debug().Str("path", change.Path).Msg("writing file")
|
||||
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
|
||||
case FileChangeResolvedActionUpdateFile:
|
||||
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
|
||||
defaultLogger.Debug().Str("path", change.Path).Msg("updating file")
|
||||
err := os.WriteFile(change.Path, change.ExpectedContent, 0644)
|
||||
if err != nil && strings.Contains(err.Error(), "device or resource busy") {
|
||||
defaultLogger.Error().
|
||||
Str("path", change.Path).
|
||||
Str("content", contentPreview).
|
||||
Msg("device or resource busy - gadget may be bound to UDC")
|
||||
return fmt.Errorf("%w (hint: gadget may be bound to UDC, try unbinding first)", err)
|
||||
}
|
||||
return err
|
||||
case FileChangeResolvedActionCreateFile:
|
||||
defaultLogger.Debug().Str("path", change.Path).Msg("creating file")
|
||||
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
|
||||
case FileChangeResolvedActionCreateSymlink:
|
||||
return os.Symlink(string(change.ExpectedContent), change.Path)
|
||||
target := string(change.ExpectedContent)
|
||||
defaultLogger.Debug().
|
||||
Str("path", change.Path).
|
||||
Str("target", target).
|
||||
Msg("creating symlink")
|
||||
return os.Symlink(target, change.Path)
|
||||
case FileChangeResolvedActionRecreateSymlink:
|
||||
target := string(change.ExpectedContent)
|
||||
defaultLogger.Debug().
|
||||
Str("path", change.Path).
|
||||
Str("target", target).
|
||||
Msg("recreating symlink")
|
||||
if err := os.Remove(change.Path); err != nil {
|
||||
return fmt.Errorf("failed to remove symlink: %w", err)
|
||||
}
|
||||
return os.Symlink(string(change.ExpectedContent), change.Path)
|
||||
return os.Symlink(target, change.Path)
|
||||
case FileChangeResolvedActionReorderSymlinks:
|
||||
defaultLogger.Debug().
|
||||
Str("path", change.Path).
|
||||
Int("symlink_count", len(change.ParamSymlinks)).
|
||||
Msg("reordering symlinks")
|
||||
return recreateSymlinks(change, nil)
|
||||
case FileChangeResolvedActionCreateDirectory:
|
||||
defaultLogger.Debug().Str("path", change.Path).Msg("creating directory")
|
||||
return os.MkdirAll(change.Path, 0755)
|
||||
case FileChangeResolvedActionRemove:
|
||||
defaultLogger.Debug().Str("path", change.Path).Msg("removing file")
|
||||
return os.Remove(change.Path)
|
||||
case FileChangeResolvedActionRemoveDirectory:
|
||||
defaultLogger.Debug().Str("path", change.Path).Msg("removing directory")
|
||||
return os.RemoveAll(change.Path)
|
||||
case FileChangeResolvedActionTouch:
|
||||
defaultLogger.Debug().Str("path", change.Path).Msg("touching file")
|
||||
return os.Chtimes(change.Path, time.Now(), time.Now())
|
||||
case FileChangeResolvedActionMountConfigFS:
|
||||
defaultLogger.Debug().Str("path", change.Path).Msg("mounting configfs")
|
||||
return mountConfigFS(change.Path)
|
||||
case FileChangeResolvedActionMountFunctionFS:
|
||||
defaultLogger.Debug().Str("path", change.Path).Msg("mounting functionfs")
|
||||
return mountFunctionFS(change.Path)
|
||||
case FileChangeResolvedActionDoNothing:
|
||||
return nil
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type gadgetConfigItem struct {
|
||||
@@ -238,19 +240,73 @@ func (u *UsbGadget) Init() error {
|
||||
|
||||
udcs := getUdcs()
|
||||
if len(udcs) < 1 {
|
||||
u.log.Warn().Msg("no UDC found, skipping USB stack init")
|
||||
return u.logWarn("no udc found, skipping USB stack init", nil)
|
||||
}
|
||||
|
||||
u.udc = udcs[0]
|
||||
|
||||
if err := u.ensureGadgetUnbound(); err != nil {
|
||||
u.log.Warn().Err(err).Msg("failed to ensure gadget is unbound, will continue")
|
||||
} else {
|
||||
u.log.Info().Msg("gadget unbind check completed")
|
||||
}
|
||||
|
||||
err := u.configureUsbGadget(false)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).
|
||||
Str("udc", u.udc).
|
||||
Interface("enabled_devices", u.enabledDevices).
|
||||
Msg("USB gadget initialization FAILED")
|
||||
return u.logError("unable to initialize USB stack", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) ensureGadgetUnbound() error {
|
||||
udcPath := path.Join(u.kvmGadgetPath, "UDC")
|
||||
|
||||
if _, err := os.Stat(u.kvmGadgetPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
udcContent, err := os.ReadFile(udcPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read UDC file: %w", err)
|
||||
}
|
||||
|
||||
currentUDC := strings.TrimSpace(string(udcContent))
|
||||
if currentUDC == "" || currentUDC == "none" {
|
||||
return nil
|
||||
}
|
||||
|
||||
u.log.Info().
|
||||
Str("current_udc", currentUDC).
|
||||
Str("target_udc", u.udc).
|
||||
Msg("unbinding existing UDC before reconfiguration")
|
||||
|
||||
if err := u.UnbindUDC(); err != nil {
|
||||
u.log.Warn().Err(err).Msg("failed to unbind via UDC file, trying DWC3")
|
||||
if err := u.UnbindUDCToDWC3(); err != nil {
|
||||
return fmt.Errorf("failed to unbind UDC: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if content, err := os.ReadFile(udcPath); err == nil {
|
||||
if strings.TrimSpace(string(content)) != "none" {
|
||||
u.log.Warn().Msg("UDC still bound after unbind attempt")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UsbGadget) UpdateGadgetConfig() error {
|
||||
u.configLock.Lock()
|
||||
defer u.configLock.Unlock()
|
||||
@@ -266,13 +322,48 @@ func (u *UsbGadget) UpdateGadgetConfig() error {
|
||||
}
|
||||
|
||||
func (u *UsbGadget) configureUsbGadget(resetUsb bool) error {
|
||||
u.log.Info().
|
||||
Bool("reset_usb", resetUsb).
|
||||
Msg("configuring USB gadget via transaction")
|
||||
|
||||
return u.WithTransaction(func() error {
|
||||
u.log.Info().Msg("Transaction: Mounting configfs")
|
||||
u.tx.MountConfigFS()
|
||||
|
||||
u.log.Info().Msg("Transaction: Creating config path")
|
||||
u.tx.CreateConfigPath()
|
||||
|
||||
u.log.Info().Msg("Transaction: Writing gadget configuration")
|
||||
u.tx.WriteGadgetConfig()
|
||||
|
||||
if resetUsb {
|
||||
u.log.Info().Msg("Transaction: Rebinding USB")
|
||||
u.tx.RebindUsb(true)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UsbGadget) VerifyMassStorage() error {
|
||||
if !u.enabledDevices.MassStorage {
|
||||
return nil
|
||||
}
|
||||
|
||||
massStoragePath := path.Join(u.kvmGadgetPath, "functions/mass_storage.usb0")
|
||||
if _, err := os.Stat(massStoragePath); err != nil {
|
||||
return fmt.Errorf("mass_storage function not found: %w", err)
|
||||
}
|
||||
|
||||
lunPath := path.Join(massStoragePath, "lun.0")
|
||||
if _, err := os.Stat(lunPath); err != nil {
|
||||
return fmt.Errorf("mass_storage LUN not found: %w", err)
|
||||
}
|
||||
|
||||
configLink := path.Join(u.configC1Path, "mass_storage.usb0")
|
||||
if _, err := os.Lstat(configLink); err != nil {
|
||||
return fmt.Errorf("mass_storage symlink not found: %w", err)
|
||||
}
|
||||
|
||||
u.log.Info().Msg("mass storage verified")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -151,7 +152,10 @@ func (tx *UsbGadgetTransaction) CreateConfigPath() {
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) WriteGadgetConfig() {
|
||||
tx.log.Info().Msg("=== Building USB gadget configuration ===")
|
||||
|
||||
// create kvm gadget path
|
||||
tx.log.Info().Str("path", tx.kvmGadgetPath).Msg("creating kvm gadget path")
|
||||
tx.mkdirAll(
|
||||
"gadget",
|
||||
tx.kvmGadgetPath,
|
||||
@@ -162,22 +166,43 @@ func (tx *UsbGadgetTransaction) WriteGadgetConfig() {
|
||||
deps := make([]string, 0)
|
||||
deps = append(deps, tx.kvmGadgetPath)
|
||||
|
||||
enabledCount := 0
|
||||
disabledCount := 0
|
||||
for _, val := range tx.orderedConfigItems {
|
||||
key := val.key
|
||||
item := val.item
|
||||
|
||||
// check if the item is enabled in the config
|
||||
if !tx.isGadgetConfigItemEnabled(key) {
|
||||
tx.log.Debug().
|
||||
Str("key", key).
|
||||
Str("device", item.device).
|
||||
Msg("disabling gadget item (not enabled in config)")
|
||||
tx.DisableGadgetItemConfig(item)
|
||||
disabledCount++
|
||||
continue
|
||||
}
|
||||
|
||||
tx.log.Info().
|
||||
Str("key", key).
|
||||
Str("device", item.device).
|
||||
Uint("order", item.order).
|
||||
Msg("configuring gadget item")
|
||||
deps = tx.writeGadgetItemConfig(item, deps)
|
||||
enabledCount++
|
||||
}
|
||||
|
||||
tx.log.Info().
|
||||
Int("enabled_items", enabledCount).
|
||||
Int("disabled_items", disabledCount).
|
||||
Msg("gadget items configuration completed")
|
||||
|
||||
if tx.isGadgetConfigItemEnabled("mtp") {
|
||||
tx.log.Info().Msg("MTP enabled, mounting functionfs and binding UDC")
|
||||
tx.MountFunctionFS()
|
||||
tx.WriteUDC(true)
|
||||
} else {
|
||||
tx.log.Info().Msg("MTP disabled, binding UDC directly")
|
||||
tx.WriteUDC(false)
|
||||
}
|
||||
}
|
||||
@@ -226,6 +251,22 @@ func (tx *UsbGadgetTransaction) writeGadgetItemConfig(item gadgetConfigItem, dep
|
||||
beforeChange = append(beforeChange, tx.getDisableKeys()...)
|
||||
}
|
||||
|
||||
// 对于 mass storage LUN 属性,需要确保 UDC 未绑定
|
||||
needsUnbind := strings.Contains(gadgetItemPath, "mass_storage") && strings.Contains(gadgetItemPath, "lun.")
|
||||
if needsUnbind {
|
||||
// 添加一个 unbind UDC 的步骤(如果已绑定)
|
||||
udcPath := path.Join(tx.kvmGadgetPath, "UDC")
|
||||
tx.addFileChange("udc-unbind", RequestedFileChange{
|
||||
Key: "udc-unbind-check",
|
||||
Path: udcPath,
|
||||
ExpectedState: FileStateFile,
|
||||
Description: "check and unbind UDC if needed",
|
||||
DependsOn: files,
|
||||
When: "beforeChange", // 只在需要时执行
|
||||
})
|
||||
beforeChange = append(beforeChange, "udc-unbind-check")
|
||||
}
|
||||
|
||||
if len(item.attrs) > 0 {
|
||||
// write attributes for the item
|
||||
files = append(files, tx.writeGadgetAttrs(
|
||||
@@ -355,9 +396,12 @@ func (tx *UsbGadgetTransaction) WriteUDC(mtpServer bool) {
|
||||
}
|
||||
|
||||
func (tx *UsbGadgetTransaction) RebindUsb(ignoreUnbindError bool) {
|
||||
unbindPath := path.Join(tx.dwc3Path, "unbind")
|
||||
bindPath := path.Join(tx.dwc3Path, "bind")
|
||||
|
||||
// remove the gadget from the UDC
|
||||
tx.addFileChange("udc", RequestedFileChange{
|
||||
Path: path.Join(tx.dwc3Path, "unbind"),
|
||||
Path: unbindPath,
|
||||
ExpectedState: FileStateFileWrite,
|
||||
ExpectedContent: []byte(tx.udc),
|
||||
Description: "unbind UDC",
|
||||
@@ -366,10 +410,10 @@ func (tx *UsbGadgetTransaction) RebindUsb(ignoreUnbindError bool) {
|
||||
})
|
||||
// bind the gadget to the UDC
|
||||
tx.addFileChange("udc", RequestedFileChange{
|
||||
Path: path.Join(tx.dwc3Path, "bind"),
|
||||
Path: bindPath,
|
||||
ExpectedState: FileStateFileWrite,
|
||||
ExpectedContent: []byte(tx.udc),
|
||||
Description: "bind UDC",
|
||||
DependsOn: []string{path.Join(tx.dwc3Path, "unbind")},
|
||||
DependsOn: []string{unbindPath},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package usbgadget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -118,6 +120,10 @@ func (u *UsbGadget) SetOnKeyboardStateChange(f func(state KeyboardState)) {
|
||||
u.onKeyboardStateChange = &f
|
||||
}
|
||||
|
||||
func (u *UsbGadget) SetOnHidDeviceMissing(f func(device string, err error)) {
|
||||
u.onHidDeviceMissing = &f
|
||||
}
|
||||
|
||||
func (u *UsbGadget) GetKeyboardState() KeyboardState {
|
||||
u.keyboardStateLock.Lock()
|
||||
defer u.keyboardStateLock.Unlock()
|
||||
@@ -177,6 +183,16 @@ func (u *UsbGadget) openKeyboardHidFile() error {
|
||||
var err error
|
||||
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) || strings.Contains(err.Error(), "no such file or directory") || strings.Contains(err.Error(), "no such device") {
|
||||
u.log.Error().
|
||||
Str("device", "hidg0").
|
||||
Str("device_name", "keyboard").
|
||||
Err(err).
|
||||
Msg("HID device file missing, gadget may need reinitialization")
|
||||
if u.onHidDeviceMissing != nil {
|
||||
(*u.onHidDeviceMissing)("keyboard", err)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to open hidg0: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package usbgadget
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var absoluteMouseConfig = gadgetConfigItem{
|
||||
@@ -69,6 +71,17 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
|
||||
var err error
|
||||
u.absMouseHidFile, err = os.OpenFile("/dev/hidg1", os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
|
||||
if errors.Is(err, os.ErrNotExist) || strings.Contains(err.Error(), "no such file or directory") || strings.Contains(err.Error(), "no such device") {
|
||||
u.log.Error().
|
||||
Str("device", "hidg1").
|
||||
Str("device_name", "absolute_mouse").
|
||||
Err(err).
|
||||
Msg("HID device file missing, gadget may need reinitialization")
|
||||
if u.onHidDeviceMissing != nil {
|
||||
(*u.onHidDeviceMissing)("absolute_mouse", err)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to open hidg1: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package usbgadget
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var relativeMouseConfig = gadgetConfigItem{
|
||||
@@ -59,7 +61,19 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
|
||||
var err error
|
||||
u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open hidg1: %w", err)
|
||||
|
||||
if errors.Is(err, os.ErrNotExist) || strings.Contains(err.Error(), "no such file or directory") || strings.Contains(err.Error(), "no such device") {
|
||||
u.log.Error().
|
||||
Str("device", "hidg2").
|
||||
Str("device_name", "relative_mouse").
|
||||
Err(err).
|
||||
Msg("HID device file missing, gadget may need reinitialization")
|
||||
|
||||
if u.onHidDeviceMissing != nil {
|
||||
(*u.onHidDeviceMissing)("relative_mouse", err)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to open hidg2: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ type UsbGadget struct {
|
||||
txLock sync.Mutex
|
||||
|
||||
onKeyboardStateChange *func(state KeyboardState)
|
||||
onHidDeviceMissing *func(device string, err error)
|
||||
|
||||
log *zerolog.Logger
|
||||
|
||||
@@ -140,8 +141,7 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
|
||||
absMouseAccumulatedWheelY: 0,
|
||||
}
|
||||
if err := g.Init(); err != nil {
|
||||
logger.Error().Err(err).Msg("failed to init USB gadget")
|
||||
return nil
|
||||
logger.Error().Err(err).Msg("failed to init USB gadget (will retry later)")
|
||||
}
|
||||
|
||||
return g
|
||||
|
||||
Reference in New Issue
Block a user