added autostart for windows and autostart for devices
This commit is contained in:
parent
e2b853840d
commit
486cf6d239
BIN
bin/usb-client
BIN
bin/usb-client
Binary file not shown.
Binary file not shown.
BIN
bin/usb-relay
BIN
bin/usb-relay
Binary file not shown.
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export PATH="$PATH:/usr/local/go/bin"
|
||||
|
||||
OUT="bin"
|
||||
LDFLAGS="-s -w"
|
||||
|
||||
mkdir -p "$OUT"
|
||||
|
||||
echo "=== Building Linux Client ==="
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o "$OUT/usb-client" ./cmd/usb-client/
|
||||
echo " -> $OUT/usb-client"
|
||||
|
||||
echo "=== Building Windows Client ==="
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o "$OUT/usb-client.exe" ./cmd/usb-client/
|
||||
echo " -> $OUT/usb-client.exe"
|
||||
|
||||
echo "=== Building Relay ==="
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o "$OUT/usb-relay" ./cmd/usb-relay/
|
||||
echo " -> $OUT/usb-relay"
|
||||
|
||||
echo ""
|
||||
echo "Done. Binaries in $OUT/"
|
||||
ls -lh "$OUT/"
|
||||
|
|
@ -209,7 +209,7 @@ func cmdRun(mode string) {
|
|||
}()
|
||||
go sm.Run()
|
||||
} else {
|
||||
um := client.NewUseManager(c)
|
||||
um := client.NewUseManager(c, cfg, cfgPath)
|
||||
webHandler.GetDevices = func() interface{} {
|
||||
available := um.GetAvailableDevices()
|
||||
attached := um.GetAttachedDevices()
|
||||
|
|
@ -231,11 +231,15 @@ func cmdRun(mode string) {
|
|||
var attachList []map[string]interface{}
|
||||
for _, d := range attached {
|
||||
attachList = append(attachList, map[string]interface{}{
|
||||
"bus_id": d.BusID,
|
||||
"client_id": d.ClientID,
|
||||
"client_name": d.ClientName,
|
||||
"tunnel_id": d.TunnelID,
|
||||
"vhci_port": d.VHCIPort,
|
||||
"bus_id": d.BusID,
|
||||
"vendor_id": d.VendorID,
|
||||
"product_id": d.ProductID,
|
||||
"name": d.Name,
|
||||
"client_id": d.ClientID,
|
||||
"client_name": d.ClientName,
|
||||
"tunnel_id": d.TunnelID,
|
||||
"vhci_port": d.VHCIPort,
|
||||
"auto_connect": um.IsAutoConnect(d.VendorID, d.ProductID),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -247,6 +251,8 @@ func cmdRun(mode string) {
|
|||
}
|
||||
webHandler.AttachDevice = um.AttachDevice
|
||||
webHandler.DetachDevice = um.DetachDevice
|
||||
webHandler.SetAutoConnect = um.SetAutoConnect
|
||||
webHandler.IsAutoConnect = um.IsAutoConnect
|
||||
webHandler.GetStatus = func() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"connected": true,
|
||||
|
|
@ -278,7 +284,7 @@ func cmdRun(mode string) {
|
|||
sm := client.NewShareManager(c)
|
||||
go sm.Run()
|
||||
} else {
|
||||
client.NewUseManager(c)
|
||||
client.NewUseManager(c, cfg, cfgPath)
|
||||
}
|
||||
go func() {
|
||||
if err := c.Run(); err != nil {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/duffy/usb-server/internal/config"
|
||||
"github.com/duffy/usb-server/internal/protocol"
|
||||
"github.com/duffy/usb-server/internal/usbip"
|
||||
"github.com/google/uuid"
|
||||
|
|
@ -28,6 +30,8 @@ type AttachedDevice struct {
|
|||
// UseManager handles receiving/using remote USB devices
|
||||
type UseManager struct {
|
||||
client *Client
|
||||
cfg *config.Config
|
||||
cfgPath string
|
||||
mu sync.RWMutex
|
||||
available map[string][]RemoteDevice // clientID -> devices
|
||||
attached map[string]*AttachedDevice // busID@clientID -> attached info
|
||||
|
|
@ -44,9 +48,11 @@ type useTunnel struct {
|
|||
}
|
||||
|
||||
// NewUseManager creates a use manager
|
||||
func NewUseManager(client *Client) *UseManager {
|
||||
func NewUseManager(client *Client, cfg *config.Config, cfgPath string) *UseManager {
|
||||
um := &UseManager{
|
||||
client: client,
|
||||
cfg: cfg,
|
||||
cfgPath: cfgPath,
|
||||
available: make(map[string][]RemoteDevice),
|
||||
attached: make(map[string]*AttachedDevice),
|
||||
tunnels: make(map[string]*useTunnel),
|
||||
|
|
@ -210,15 +216,20 @@ func (um *UseManager) setupVHCI(clientID, busID string, granted *protocol.Device
|
|||
}
|
||||
|
||||
key := busID + "@" + clientID
|
||||
remDev := RemoteDevice{
|
||||
USBDevice: protocol.USBDevice{BusID: busID},
|
||||
ClientID: clientID,
|
||||
}
|
||||
if devInfo != nil {
|
||||
remDev = *devInfo
|
||||
}
|
||||
|
||||
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: vhciPort,
|
||||
RemoteDevice: remDev,
|
||||
TunnelID: granted.TunnelID,
|
||||
VHCIPort: vhciPort,
|
||||
}
|
||||
um.mu.Unlock()
|
||||
|
||||
|
|
@ -273,10 +284,102 @@ func (um *UseManager) handleDeviceList(msg *protocol.DeviceList) {
|
|||
})
|
||||
}
|
||||
um.available[msg.ClientID] = remoteDevs
|
||||
|
||||
// Collect devices to auto-connect (while holding the lock to check attached map)
|
||||
var toAutoConnect []RemoteDevice
|
||||
for _, dev := range remoteDevs {
|
||||
if dev.Status != protocol.StatusAvailable {
|
||||
continue
|
||||
}
|
||||
key := dev.BusID + "@" + msg.ClientID
|
||||
if _, attached := um.attached[key]; attached {
|
||||
continue
|
||||
}
|
||||
if um.matchesAutoConnect(dev) {
|
||||
toAutoConnect = append(toAutoConnect, dev)
|
||||
}
|
||||
}
|
||||
um.mu.Unlock()
|
||||
|
||||
log.Printf("[use] received device list from %s (%s): %d devices",
|
||||
msg.ClientName, msg.ClientID[:8], len(msg.Devices))
|
||||
|
||||
// Auto-connect matching devices (outside lock, each in its own goroutine)
|
||||
for _, dev := range toAutoConnect {
|
||||
log.Printf("[use] auto-connecting %s (%s:%s) from %s",
|
||||
dev.Name, dev.VendorID, dev.ProductID, msg.ClientName)
|
||||
go um.AttachDevice(msg.ClientID, dev.BusID)
|
||||
}
|
||||
}
|
||||
|
||||
// matchesAutoConnect checks if a device matches any auto-connect rule.
|
||||
// Must be called with um.mu held (at least RLock).
|
||||
func (um *UseManager) matchesAutoConnect(dev RemoteDevice) bool {
|
||||
for _, rule := range um.cfg.AutoConnect {
|
||||
if rule.VendorID != "" && !strings.EqualFold(rule.VendorID, dev.VendorID) {
|
||||
continue
|
||||
}
|
||||
if rule.ProductID != "" && !strings.EqualFold(rule.ProductID, dev.ProductID) {
|
||||
continue
|
||||
}
|
||||
if rule.BusID != "" && rule.BusID != dev.BusID {
|
||||
continue
|
||||
}
|
||||
if rule.ClientName != "" && rule.ClientName != dev.ClientName {
|
||||
continue
|
||||
}
|
||||
return true // all specified fields match
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SetAutoConnect adds or removes an auto-connect rule for a VendorID:ProductID pair.
|
||||
func (um *UseManager) SetAutoConnect(vendorID, productID string, enabled bool) error {
|
||||
um.mu.Lock()
|
||||
defer um.mu.Unlock()
|
||||
|
||||
if enabled {
|
||||
// Check if rule already exists
|
||||
for _, rule := range um.cfg.AutoConnect {
|
||||
if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) {
|
||||
return nil // already exists
|
||||
}
|
||||
}
|
||||
um.cfg.AutoConnect = append(um.cfg.AutoConnect, config.AutoConnectRule{
|
||||
VendorID: vendorID,
|
||||
ProductID: productID,
|
||||
})
|
||||
} else {
|
||||
// Remove matching rule
|
||||
filtered := um.cfg.AutoConnect[:0]
|
||||
for _, rule := range um.cfg.AutoConnect {
|
||||
if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, rule)
|
||||
}
|
||||
um.cfg.AutoConnect = filtered
|
||||
}
|
||||
|
||||
if err := um.cfg.Save(um.cfgPath); err != nil {
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[use] auto-connect %s:%s = %v", vendorID, productID, enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsAutoConnect checks if there is an auto-connect rule for this VendorID:ProductID.
|
||||
func (um *UseManager) IsAutoConnect(vendorID, productID string) bool {
|
||||
um.mu.RLock()
|
||||
defer um.mu.RUnlock()
|
||||
|
||||
for _, rule := range um.cfg.AutoConnect {
|
||||
if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (um *UseManager) handleDeviceGranted(msg *protocol.DeviceGranted) {
|
||||
|
|
|
|||
|
|
@ -2,16 +2,81 @@
|
|||
|
||||
package service
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const taskName = "USB-Client"
|
||||
|
||||
// Install creates a Windows Scheduled Task that runs at system startup
|
||||
func Install(mode, configPath string) error {
|
||||
return fmt.Errorf("Windows service installation not yet implemented")
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding executable: %w", err)
|
||||
}
|
||||
exePath, err = filepath.Abs(exePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving path: %w", err)
|
||||
}
|
||||
|
||||
absConfigPath, err := filepath.Abs(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving config path: %w", err)
|
||||
}
|
||||
|
||||
// Remove previous task if it exists
|
||||
exec.Command("schtasks", "/Delete", "/TN", taskName, "/F").Run()
|
||||
|
||||
// Create task: run at system startup, as SYSTEM, with highest privileges
|
||||
taskCmd := fmt.Sprintf(`"%s" %s --config "%s" --no-gui`, exePath, mode, absConfigPath)
|
||||
cmd := exec.Command("schtasks", "/Create",
|
||||
"/SC", "ONSTART",
|
||||
"/TN", taskName,
|
||||
"/TR", taskCmd,
|
||||
"/RU", "SYSTEM",
|
||||
"/RL", "HIGHEST",
|
||||
"/F",
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating scheduled task: %w (output: %s)", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
// Start the task immediately
|
||||
startCmd := exec.Command("schtasks", "/Run", "/TN", taskName)
|
||||
startOutput, err := startCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting task: %w (output: %s)", err, strings.TrimSpace(string(startOutput)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uninstall stops and removes the Scheduled Task
|
||||
func Uninstall() error {
|
||||
return fmt.Errorf("Windows service uninstallation not yet implemented")
|
||||
// Stop the running task
|
||||
exec.Command("schtasks", "/End", "/TN", taskName).Run()
|
||||
|
||||
// Delete the task
|
||||
cmd := exec.Command("schtasks", "/Delete", "/TN", taskName, "/F")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting scheduled task: %w (output: %s)", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status returns the current status of the Scheduled Task
|
||||
func Status() (string, error) {
|
||||
return "not implemented on Windows", nil
|
||||
cmd := exec.Command("schtasks", "/Query", "/TN", taskName, "/FO", "LIST")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "Task nicht gefunden", nil
|
||||
}
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ type Handler struct {
|
|||
GetDevices func() interface{}
|
||||
AttachDevice func(clientID, busID string) error
|
||||
DetachDevice func(clientID, busID string) error
|
||||
SetAutoConnect func(vendorID, productID string, enabled bool) error
|
||||
IsAutoConnect func(vendorID, productID string) bool
|
||||
InstallService func() error
|
||||
UninstallService func() error
|
||||
GetStatus func() map[string]interface{}
|
||||
|
|
@ -54,6 +56,7 @@ func (h *Handler) setupRoutes() {
|
|||
h.mux.HandleFunc("/api/config", h.handleConfig)
|
||||
h.mux.HandleFunc("/api/generate-token", h.handleGenerateToken)
|
||||
h.mux.HandleFunc("/api/apply-tokens", h.handleApplyTokens)
|
||||
h.mux.HandleFunc("/api/auto-connect", h.handleAutoConnect)
|
||||
h.mux.HandleFunc("/api/service/install", h.handleServiceInstall)
|
||||
h.mux.HandleFunc("/api/service/uninstall", h.handleServiceUninstall)
|
||||
}
|
||||
|
|
@ -252,6 +255,35 @@ func (h *Handler) handleApplyTokens(w http.ResponseWriter, r *http.Request) {
|
|||
writeJSON(w, map[string]interface{}{"ok": true, "hash": hash})
|
||||
}
|
||||
|
||||
func (h *Handler) handleAutoConnect(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
VendorID string `json:"vendor_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
if h.SetAutoConnect == nil {
|
||||
writeJSON(w, map[string]interface{}{"ok": false, "error": "not in use mode"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.SetAutoConnect(req.VendorID, req.ProductID, req.Enabled); err != nil {
|
||||
writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, map[string]interface{}{"ok": true})
|
||||
}
|
||||
|
||||
func (h *Handler) handleServiceInstall(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
|
|
|
|||
|
|
@ -94,13 +94,19 @@ function renderUseDevices(container, available, attached) {
|
|||
html += attached.map(dev => `
|
||||
<div class="device-card">
|
||||
<div class="device-info">
|
||||
<div class="device-name">${escapeHtml(dev.bus_id)}</div>
|
||||
<div class="device-name">${escapeHtml(dev.name || dev.bus_id)}</div>
|
||||
<div class="device-details">
|
||||
<span>Von: ${escapeHtml(dev.client_name || dev.client_id)}</span>
|
||||
${dev.vendor_id ? `<span>VID:PID: ${dev.vendor_id}:${dev.product_id}</span>` : ''}
|
||||
<span>VHCI Port: ${dev.vhci_port}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-status">
|
||||
<label class="auto-connect-label" title="Beim Start automatisch verbinden">
|
||||
<input type="checkbox" ${dev.auto_connect ? 'checked' : ''}
|
||||
onchange="toggleAutoConnect('${dev.vendor_id}', '${dev.product_id}', this.checked)">
|
||||
Autostart
|
||||
</label>
|
||||
<span class="badge attached">Verbunden</span>
|
||||
<button class="btn small danger" onclick="detachDevice('${dev.client_id}', '${dev.bus_id}')">Trennen</button>
|
||||
</div>
|
||||
|
|
@ -191,6 +197,25 @@ async function detachDevice(clientId, busId) {
|
|||
}
|
||||
}
|
||||
|
||||
// Auto-connect toggle
|
||||
async function toggleAutoConnect(vendorId, productId, enabled) {
|
||||
try {
|
||||
const resp = await fetch(API_BASE + '/api/auto-connect', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ vendor_id: vendorId, product_id: productId, enabled: enabled })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!data.ok) {
|
||||
alert('Fehler: ' + (data.error || 'Unbekannt'));
|
||||
updateDevices();
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Fehler: ' + e.message);
|
||||
updateDevices();
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
async function loadSettings() {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -279,3 +279,18 @@ nav {
|
|||
color: #666;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.auto-connect-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.auto-connect-label input[type="checkbox"] {
|
||||
accent-color: #00d4ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue