kvmscan.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package main
  2. import (
  3. "errors"
  4. "os/exec"
  5. "path/filepath"
  6. "regexp"
  7. "strings"
  8. )
  9. /*
  10. Each of the USB-KVM device has the same set of USB devices
  11. connected under a single USB hub chip. This function
  12. will scan the USB device tree to find the connected
  13. USB devices and match them to the configured device paths.
  14. Commonly found devices are:
  15. - USB hub (the main hub chip)
  16. -- USB UART device (HID KVM)
  17. -- USB CDC ACM device (auxiliary MCU)
  18. -- USB Video Class device (webcam capture)
  19. -- USB Audio Class device (audio capture)
  20. */
  21. type UsbKvmDevice struct {
  22. USBKVMDevicePath string
  23. AuxMCUDevicePath string
  24. CaptureDevicePaths []string
  25. AlsaDevicePaths []string
  26. }
  27. func discoverUsbKvmSubtree() ([]*UsbKvmDevice, error) {
  28. // Scan all /dev/tty*, /dev/video*, /dev/snd/pcmC* devices
  29. getMatchingDevs := func(pattern string) ([]string, error) {
  30. files, err := filepath.Glob(pattern)
  31. if err != nil {
  32. return nil, err
  33. }
  34. return files, nil
  35. }
  36. // Get all ttyUSB*, ttyACM*
  37. ttyDevs1, _ := getMatchingDevs("/dev/ttyUSB*")
  38. ttyDevs2, _ := getMatchingDevs("/dev/ttyACM*")
  39. ttyDevs := append(ttyDevs1, ttyDevs2...)
  40. // Get all video*
  41. videoDevs, _ := getMatchingDevs("/dev/video*")
  42. // Get all ALSA PCM devices (USB audio is usually card > 0)
  43. alsaDevs, _ := getMatchingDevs("/dev/snd/pcmC*")
  44. type devInfo struct {
  45. path string
  46. sysPath string
  47. }
  48. getSys := func(devs []string) []devInfo {
  49. var out []devInfo
  50. for _, d := range devs {
  51. sys, err := getDeviceFullPath(d)
  52. if err == nil {
  53. out = append(out, devInfo{d, sys})
  54. }
  55. }
  56. return out
  57. }
  58. ttys := getSys(ttyDevs)
  59. videos := getSys(videoDevs)
  60. alsas := getSys(alsaDevs)
  61. // Find common USB root hub prefix
  62. getHub := func(sys string) string {
  63. parts := strings.Split(sys, "/")
  64. for i := range parts {
  65. // Look for USB hub pattern (e.g. 1-2, 2-1, etc.)
  66. if matched, _ := regexp.MatchString(`^\d+-\d+(\.\d+)*$`, parts[i]); matched {
  67. return strings.Join(parts[:i+1], "/")
  68. }
  69. }
  70. return ""
  71. }
  72. // Map hub -> device info
  73. type hubGroup struct {
  74. ttys []string
  75. acms []string
  76. videos []string
  77. alsas []string
  78. }
  79. hubs := make(map[string]*hubGroup)
  80. for _, t := range ttys {
  81. hub := getHub(t.sysPath)
  82. if hub != "" {
  83. if hubs[hub] == nil {
  84. hubs[hub] = &hubGroup{}
  85. }
  86. if strings.Contains(t.path, "ACM") {
  87. hubs[hub].acms = append(hubs[hub].acms, t.path)
  88. } else {
  89. hubs[hub].ttys = append(hubs[hub].ttys, t.path)
  90. }
  91. }
  92. }
  93. for _, v := range videos {
  94. hub := getHub(v.sysPath)
  95. if hub != "" {
  96. if hubs[hub] == nil {
  97. hubs[hub] = &hubGroup{}
  98. }
  99. hubs[hub].videos = append(hubs[hub].videos, v.path)
  100. }
  101. }
  102. for _, alsa := range alsas {
  103. hub := getHub(alsa.sysPath)
  104. if hub != "" {
  105. if hubs[hub] == nil {
  106. hubs[hub] = &hubGroup{}
  107. }
  108. hubs[hub].alsas = append(hubs[hub].alsas, alsa.path)
  109. }
  110. }
  111. var result []*UsbKvmDevice
  112. for _, g := range hubs {
  113. // At least one tty or acm, one video, optionally alsa
  114. if (len(g.ttys) > 0 || len(g.acms) > 0) && len(g.videos) > 0 {
  115. // Pick the first tty as USBKVMDevicePath, first acm as AuxMCUDevicePath
  116. usbKvm := ""
  117. auxMcu := ""
  118. if len(g.ttys) > 0 {
  119. usbKvm = g.ttys[0]
  120. }
  121. if len(g.acms) > 0 {
  122. auxMcu = g.acms[0]
  123. }
  124. result = append(result, &UsbKvmDevice{
  125. USBKVMDevicePath: usbKvm,
  126. AuxMCUDevicePath: auxMcu,
  127. CaptureDevicePaths: g.videos,
  128. AlsaDevicePaths: g.alsas,
  129. })
  130. }
  131. }
  132. if len(result) == 0 {
  133. return nil, errors.New("no USB KVM device found")
  134. }
  135. return result, nil
  136. }
  137. func resolveSymlink(path string) (string, error) {
  138. resolved, err := filepath.EvalSymlinks(path)
  139. if err != nil {
  140. return "", err
  141. }
  142. return resolved, nil
  143. }
  144. func getDeviceFullPath(devicePath string) (string, error) {
  145. resolvedPath, err := resolveSymlink(devicePath)
  146. if err != nil {
  147. return "", err
  148. }
  149. // Use udevadm to get the device chain
  150. out, err := exec.Command("udevadm", "info", "-q", "path", "-n", resolvedPath).Output()
  151. if err != nil {
  152. return "", err
  153. }
  154. sysPath := strings.TrimSpace(string(out))
  155. if sysPath == "" {
  156. return "", errors.New("could not resolve sysfs path")
  157. }
  158. fullPath := "/sys" + sysPath
  159. return fullPath, nil
  160. }