usb-server/internal/client/use.go

369 lines
8.7 KiB
Go

package client
import (
"fmt"
"log"
"net"
"sync"
"github.com/duffy/usb-server/internal/protocol"
"github.com/duffy/usb-server/internal/usbip"
"github.com/google/uuid"
)
// RemoteDevice represents a USB device available from a share client
type RemoteDevice struct {
protocol.USBDevice
ClientID string `json:"client_id"`
ClientName string `json:"client_name"`
}
// AttachedDevice represents a device currently attached via VHCI
type AttachedDevice struct {
RemoteDevice
TunnelID string `json:"tunnel_id"`
VHCIPort int `json:"vhci_port"`
SocketFD int `json:"socket_fd"`
}
// UseManager handles receiving/using remote USB devices
type UseManager struct {
client *Client
mu sync.RWMutex
available map[string][]RemoteDevice // clientID -> devices
attached map[string]*AttachedDevice // busID@clientID -> attached info
tunnels map[string]*useTunnel // tunnelID -> tunnel
pending map[string]chan *protocol.DeviceGranted // requestID -> response channel
}
type useTunnel struct {
id string
busID string
clientID string
conn net.Conn // our end of the socketpair
done chan struct{}
}
// NewUseManager creates a use manager
func NewUseManager(client *Client) *UseManager {
um := &UseManager{
client: client,
available: make(map[string][]RemoteDevice),
attached: make(map[string]*AttachedDevice),
tunnels: make(map[string]*useTunnel),
pending: make(map[string]chan *protocol.DeviceGranted),
}
client.OnDeviceList = um.handleDeviceList
client.OnDeviceGranted = um.handleDeviceGranted
client.OnDeviceDenied = um.handleDeviceDenied
client.OnDeviceReleased = um.handleDeviceReleased
client.OnTunnelData = um.handleTunnelData
client.OnClientLeft = um.handleClientLeft
return um
}
// GetAvailableDevices returns all available remote devices
func (um *UseManager) GetAvailableDevices() []RemoteDevice {
um.mu.RLock()
defer um.mu.RUnlock()
var all []RemoteDevice
for _, devs := range um.available {
all = append(all, devs...)
}
return all
}
// GetAttachedDevices returns currently attached devices
func (um *UseManager) GetAttachedDevices() []*AttachedDevice {
um.mu.RLock()
defer um.mu.RUnlock()
var result []*AttachedDevice
for _, dev := range um.attached {
result = append(result, dev)
}
return result
}
// AttachDevice requests and attaches a remote USB device
func (um *UseManager) AttachDevice(clientID, busID string) error {
// Check if VHCI is available
if !usbip.IsVHCIAvailable() {
return fmt.Errorf("vhci-hcd kernel module not loaded (run: sudo modprobe vhci-hcd)")
}
key := busID + "@" + clientID
um.mu.RLock()
if _, already := um.attached[key]; already {
um.mu.RUnlock()
return fmt.Errorf("device %s already attached", key)
}
um.mu.RUnlock()
// Create request
requestID := uuid.New().String()
respChan := make(chan *protocol.DeviceGranted, 1)
um.mu.Lock()
um.pending[requestID] = respChan
um.mu.Unlock()
defer func() {
um.mu.Lock()
delete(um.pending, requestID)
um.mu.Unlock()
}()
// Send request to relay
err := um.client.SendJSON(&protocol.RequestDevice{
Type: protocol.MsgRequestDevice,
TargetClient: clientID,
BusID: busID,
RequestID: requestID,
})
if err != nil {
return fmt.Errorf("sending request: %w", err)
}
log.Printf("[use] requesting device %s from %s", busID, clientID)
// Wait for response (with timeout via context)
select {
case granted := <-respChan:
return um.setupVHCI(clientID, busID, granted)
case <-um.client.ctx.Done():
return fmt.Errorf("client shutting down")
}
}
// DetachDevice releases an attached device
func (um *UseManager) DetachDevice(clientID, busID string) error {
key := busID + "@" + clientID
um.mu.Lock()
dev, exists := um.attached[key]
if !exists {
um.mu.Unlock()
return fmt.Errorf("device %s not attached", key)
}
// Clean up tunnel
if tunnel, ok := um.tunnels[dev.TunnelID]; ok {
close(tunnel.done)
if tunnel.conn != nil {
tunnel.conn.Close()
}
delete(um.tunnels, dev.TunnelID)
}
// Detach from VHCI
if dev.VHCIPort >= 0 {
if err := usbip.DetachDevice(dev.VHCIPort); err != nil {
log.Printf("[use] warning: VHCI detach error: %v", err)
}
}
delete(um.attached, key)
um.mu.Unlock()
// Notify share client
um.client.SendJSON(&protocol.ReleaseDevice{
Type: protocol.MsgReleaseDevice,
TargetClient: clientID,
BusID: busID,
})
log.Printf("[use] device %s detached", key)
return nil
}
func (um *UseManager) setupVHCI(clientID, busID string, granted *protocol.DeviceGranted) error {
// Create a socketpair - one end for VHCI, one for our tunnel
fds, err := createSocketPair()
if err != nil {
return fmt.Errorf("creating socketpair: %w", err)
}
vhciFD := fds[0]
tunnelFD := fds[1]
// Find a free VHCI port
port, err := usbip.FindFreePort(granted.Speed)
if err != nil {
closeFDs(fds)
return fmt.Errorf("finding free VHCI port: %w", err)
}
// Attach to VHCI
if err := usbip.AttachDevice(port, vhciFD, granted.DevID, granted.Speed); err != nil {
closeFDs(fds)
return fmt.Errorf("VHCI attach: %w", err)
}
// The VHCI driver now owns vhciFD, so we don't close it
// Create a net.Conn from the tunnel FD
tunnelFile := fdToFile(tunnelFD, "usb-tunnel")
tunnelConn, err := net.FileConn(tunnelFile)
tunnelFile.Close() // FileConn dups the fd
if err != nil {
usbip.DetachDevice(port)
return fmt.Errorf("creating tunnel conn: %w", err)
}
tunnel := &useTunnel{
id: granted.TunnelID,
busID: busID,
clientID: clientID,
conn: tunnelConn,
done: make(chan struct{}),
}
key := busID + "@" + clientID
um.mu.Lock()
um.tunnels[granted.TunnelID] = tunnel
um.attached[key] = &AttachedDevice{
RemoteDevice: RemoteDevice{
USBDevice: protocol.USBDevice{BusID: busID},
ClientID: clientID,
},
TunnelID: granted.TunnelID,
VHCIPort: port,
SocketFD: vhciFD,
}
um.mu.Unlock()
// Start reading from the tunnel socket (VHCI -> relay)
go um.tunnelReadLoop(tunnel)
log.Printf("[use] device %s attached on VHCI port %d", key, port)
return nil
}
// tunnelReadLoop reads from the VHCI socket and sends to relay
func (um *UseManager) tunnelReadLoop(tunnel *useTunnel) {
buf := make([]byte, 65536)
for {
select {
case <-tunnel.done:
return
default:
}
n, err := tunnel.conn.Read(buf)
if err != nil {
select {
case <-tunnel.done:
return
default:
log.Printf("[use] tunnel read error: %v", err)
return
}
}
if err := um.client.SendTunnelData(tunnel.id, buf[:n]); err != nil {
return
}
}
}
func (um *UseManager) handleDeviceList(msg *protocol.DeviceList) {
um.mu.Lock()
var remoteDevs []RemoteDevice
for _, dev := range msg.Devices {
remoteDevs = append(remoteDevs, RemoteDevice{
USBDevice: dev,
ClientID: msg.ClientID,
ClientName: msg.ClientName,
})
}
um.available[msg.ClientID] = remoteDevs
um.mu.Unlock()
log.Printf("[use] received device list from %s (%s): %d devices",
msg.ClientName, msg.ClientID[:8], len(msg.Devices))
}
func (um *UseManager) handleDeviceGranted(msg *protocol.DeviceGranted) {
um.mu.RLock()
ch, exists := um.pending[msg.RequestID]
um.mu.RUnlock()
if exists {
ch <- msg
}
}
func (um *UseManager) handleDeviceDenied(msg *protocol.DeviceDenied) {
log.Printf("[use] device request denied: %s - %s", msg.BusID, msg.Reason)
um.mu.RLock()
ch, exists := um.pending[msg.RequestID]
um.mu.RUnlock()
if exists {
close(ch) // signal denial by closing channel
}
}
func (um *UseManager) handleDeviceReleased(msg *protocol.DeviceReleased) {
log.Printf("[use] device released by share client: %s", msg.BusID)
}
func (um *UseManager) handleTunnelData(tunnelID string, data []byte) {
um.mu.RLock()
tunnel, exists := um.tunnels[tunnelID]
um.mu.RUnlock()
if !exists {
return
}
// Write to the tunnel socket (relay -> VHCI)
tunnel.conn.Write(data)
}
func (um *UseManager) handleClientLeft(msg *protocol.ClientLeft) {
um.mu.Lock()
delete(um.available, msg.ClientID)
// Detach any devices from this client
for key, dev := range um.attached {
if dev.ClientID == msg.ClientID {
if tunnel, ok := um.tunnels[dev.TunnelID]; ok {
close(tunnel.done)
tunnel.conn.Close()
delete(um.tunnels, dev.TunnelID)
}
if dev.VHCIPort >= 0 {
usbip.DetachDevice(dev.VHCIPort)
}
delete(um.attached, key)
log.Printf("[use] device %s auto-detached (client left)", key)
}
}
um.mu.Unlock()
}
// Cleanup releases all attached devices
func (um *UseManager) Cleanup() {
um.mu.Lock()
defer um.mu.Unlock()
for key, dev := range um.attached {
if tunnel, ok := um.tunnels[dev.TunnelID]; ok {
close(tunnel.done)
tunnel.conn.Close()
}
if dev.VHCIPort >= 0 {
usbip.DetachDevice(dev.VHCIPort)
}
log.Printf("[use] cleaned up device %s", key)
}
um.attached = make(map[string]*AttachedDevice)
um.tunnels = make(map[string]*useTunnel)
}