added other dsconnect settings

This commit is contained in:
duffyduck 2026-02-19 11:13:14 +01:00
parent 019c60689e
commit 09af99946e
13 changed files with 204 additions and 35 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -184,7 +184,7 @@ func cmdRun(mode string) {
webHandler := web.NewHandler(cfg, cfgPath)
if mode == "share" {
sm := client.NewShareManager(c)
sm := client.NewShareManager(c, cfg)
webHandler.GetDevices = func() interface{} {
return map[string]interface{}{
"mode": "share",
@ -225,6 +225,7 @@ func cmdRun(mode string) {
"speed": d.Speed,
"client_id": d.ClientID,
"client_name": d.ClientName,
"allow_force_detach": um.IsForceDetachable(d.ClientID),
})
}
@ -251,6 +252,7 @@ func cmdRun(mode string) {
}
webHandler.AttachDevice = um.AttachDevice
webHandler.DetachDevice = um.DetachDevice
webHandler.ForceDetachDevice = um.ForceDetachDevice
webHandler.SetAutoConnect = um.SetAutoConnect
webHandler.IsAutoConnect = um.IsAutoConnect
webHandler.GetStatus = func() map[string]interface{} {
@ -281,7 +283,7 @@ func cmdRun(mode string) {
} else {
// No GUI mode
if mode == "share" {
sm := client.NewShareManager(c)
sm := client.NewShareManager(c, cfg)
go sm.Run()
} else {
client.NewUseManager(c, cfg, cfgPath)

View File

@ -31,6 +31,7 @@ type Client struct {
OnClientLeft func(msg *protocol.ClientLeft)
OnRequestDevice func(targetClient, fromClient, busID, requestID string)
OnReleaseDevice func(busID, fromClient string)
OnForceRelease func(targetClient, fromClient, busID string)
OnTunnelData func(tunnelID string, data []byte)
ctx context.Context
@ -255,6 +256,18 @@ func (c *Client) handleTextMessage(data []byte) {
}
}
case protocol.MsgForceRelease:
if c.OnForceRelease != nil {
var msg struct {
TargetClient string `json:"target_client"`
FromClient string `json:"from_client"`
BusID string `json:"bus_id"`
}
if json.Unmarshal(data, &msg) == nil {
c.OnForceRelease(msg.TargetClient, msg.FromClient, msg.BusID)
}
}
case protocol.MsgReleaseDevice:
if c.OnReleaseDevice != nil {
var msg struct {

View File

@ -7,6 +7,7 @@ import (
"sync"
"time"
"github.com/duffy/usb-server/internal/config"
"github.com/duffy/usb-server/internal/protocol"
"github.com/duffy/usb-server/internal/usb"
"github.com/duffy/usb-server/internal/usbip"
@ -16,6 +17,7 @@ import (
// ShareManager handles sharing USB devices
type ShareManager struct {
client *Client
cfg *config.Config
mu sync.RWMutex
devices []usb.Device
active map[string]*activeShare // busID -> active share
@ -38,9 +40,10 @@ type shareTunnel struct {
}
// NewShareManager creates a share manager
func NewShareManager(client *Client) *ShareManager {
func NewShareManager(client *Client, cfg *config.Config) *ShareManager {
sm := &ShareManager{
client: client,
cfg: cfg,
active: make(map[string]*activeShare),
tunnels: make(map[string]*shareTunnel),
}
@ -50,6 +53,7 @@ func NewShareManager(client *Client) *ShareManager {
client.OnReleaseDevice = sm.handleReleaseDevice
client.OnTunnelData = sm.handleTunnelData
client.OnClientLeft = sm.handleClientLeft
client.OnForceRelease = sm.handleForceRelease
return sm
}
@ -133,6 +137,7 @@ func (sm *ShareManager) broadcastDeviceList() {
ClientID: sm.client.ID(),
ClientName: sm.client.Config().Name,
Devices: protoDevices,
AllowForceDetach: sm.cfg.AllowForceDetach,
}
sm.client.SendJSON(msg)
@ -312,6 +317,24 @@ func (sm *ShareManager) handleReleaseDevice(busID, fromClient string) {
sm.broadcastDeviceList()
}
func (sm *ShareManager) handleForceRelease(targetClient, fromClient, busID string) {
if !sm.cfg.AllowForceDetach {
log.Printf("[share] force-release denied for %s (not allowed by config)", busID)
return
}
sm.mu.RLock()
share, exists := sm.active[busID]
sm.mu.RUnlock()
if !exists {
return
}
log.Printf("[share] force-releasing %s (requested by %s, was used by %s)", busID, fromClient[:8], share.usedBy[:8])
sm.handleReleaseDevice(busID, share.usedBy)
}
func (sm *ShareManager) handleClientLeft(msg *protocol.ClientLeft) {
sm.mu.RLock()
var toRelease []string

View File

@ -37,6 +37,7 @@ type UseManager struct {
attached map[string]*AttachedDevice // busID@clientID -> attached info
tunnels map[string]*useTunnel // tunnelID -> tunnel
pending map[string]chan *protocol.DeviceGranted // requestID -> response channel
forceDetachable map[string]bool // clientID -> allow_force_detach
}
type useTunnel struct {
@ -57,6 +58,7 @@ func NewUseManager(client *Client, cfg *config.Config, cfgPath string) *UseManag
attached: make(map[string]*AttachedDevice),
tunnels: make(map[string]*useTunnel),
pending: make(map[string]chan *protocol.DeviceGranted),
forceDetachable: make(map[string]bool),
}
client.OnDeviceList = um.handleDeviceList
@ -284,6 +286,7 @@ func (um *UseManager) handleDeviceList(msg *protocol.DeviceList) {
})
}
um.available[msg.ClientID] = remoteDevs
um.forceDetachable[msg.ClientID] = msg.AllowForceDetach
// Collect devices to auto-connect (while holding the lock to check attached map)
var toAutoConnect []RemoteDevice
@ -382,6 +385,22 @@ func (um *UseManager) IsAutoConnect(vendorID, productID string) bool {
return false
}
// IsForceDetachable checks if a share client allows force-detach
func (um *UseManager) IsForceDetachable(clientID string) bool {
um.mu.RLock()
defer um.mu.RUnlock()
return um.forceDetachable[clientID]
}
// ForceDetachDevice sends a force-release request to the share client
func (um *UseManager) ForceDetachDevice(clientID, busID string) error {
return um.client.SendJSON(&protocol.ForceRelease{
Type: protocol.MsgForceRelease,
TargetClient: clientID,
BusID: busID,
})
}
func (um *UseManager) handleDeviceGranted(msg *protocol.DeviceGranted) {
um.mu.RLock()
ch, exists := um.pending[msg.RequestID]
@ -430,6 +449,7 @@ func (um *UseManager) handleTunnelData(tunnelID string, data []byte) {
func (um *UseManager) handleClientLeft(msg *protocol.ClientLeft) {
um.mu.Lock()
delete(um.available, msg.ClientID)
delete(um.forceDetachable, msg.ClientID)
// Detach any devices from this client
for key, dev := range um.attached {

View File

@ -30,6 +30,9 @@ type Config struct {
// Auto-connect rules (use mode only)
AutoConnect []AutoConnectRule `json:"auto_connect,omitempty"`
// Share mode: allow other clients to force-detach devices in use
AllowForceDetach bool `json:"allow_force_detach,omitempty"`
}
// DefaultConfig returns a config with sensible defaults

View File

@ -11,6 +11,7 @@ const (
MsgDeviceReleased = "device_released"
MsgClientJoined = "client_joined"
MsgClientLeft = "client_left"
MsgForceRelease = "force_release"
MsgPing = "ping"
MsgPong = "pong"
MsgError = "error"
@ -67,6 +68,7 @@ type DeviceList struct {
ClientID string `json:"client_id"`
ClientName string `json:"client_name"`
Devices []USBDevice `json:"devices"`
AllowForceDetach bool `json:"allow_force_detach,omitempty"`
}
// RequestDevice is sent by use clients to request a specific device
@ -108,6 +110,13 @@ type DeviceReleased struct {
BusID string `json:"bus_id"`
}
// ForceRelease is sent by use clients to force-release a device from another user
type ForceRelease struct {
Type string `json:"type"`
TargetClient string `json:"target_client"`
BusID string `json:"bus_id"`
}
// ClientJoined is broadcast when a new client joins the group
type ClientJoined struct {
Type string `json:"type"`

View File

@ -128,6 +128,8 @@ func (h *Hub) HandleTextMessage(sender *Client, data []byte) {
h.handleDeviceGranted(sender, data)
case protocol.MsgDeviceDenied:
h.handleDeviceDenied(sender, data)
case protocol.MsgForceRelease:
h.handleForceRelease(sender, data)
case protocol.MsgReleaseDevice:
h.handleReleaseDevice(sender, data)
case protocol.MsgDeviceReleased:
@ -311,6 +313,41 @@ func (h *Hub) handleReleaseDevice(sender *Client, data []byte) {
h.mu.RUnlock()
}
// handleForceRelease forwards a force-release request to the target share client
func (h *Hub) handleForceRelease(sender *Client, data []byte) {
var msg protocol.ForceRelease
if err := json.Unmarshal(data, &msg); err != nil {
return
}
// Clean up tunnel for this device (by BusID, regardless of who owns it)
h.mu.Lock()
for tid, tunnel := range h.tunnels {
if tunnel.BusID == msg.BusID && tunnel.ShareClient == msg.TargetClient {
delete(h.tunnels, tid)
log.Printf("[hub] tunnel force-closed: %s", tid)
break
}
}
h.mu.Unlock()
// Forward to share client
h.mu.RLock()
group := h.groups[sender.Hash]
if group != nil {
if target := group[msg.TargetClient]; target != nil && target.Mode == protocol.ModeShare {
enriched := map[string]interface{}{
"type": protocol.MsgForceRelease,
"target_client": msg.TargetClient,
"bus_id": msg.BusID,
"from_client": sender.ID,
}
target.WriteJSON(enriched)
}
}
h.mu.RUnlock()
}
// handleDeviceReleased broadcasts device released notification
func (h *Hub) handleDeviceReleased(sender *Client, data []byte) {
h.mu.RLock()

View File

@ -26,6 +26,7 @@ type Handler struct {
DetachDevice func(clientID, busID string) error
SetAutoConnect func(vendorID, productID string, enabled bool) error
IsAutoConnect func(vendorID, productID string) bool
ForceDetachDevice func(clientID, busID string) error
InstallService func() error
UninstallService func() error
GetStatus func() map[string]interface{}
@ -57,6 +58,7 @@ func (h *Handler) setupRoutes() {
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/force-detach", h.handleForceDetach)
h.mux.HandleFunc("/api/service/install", h.handleServiceInstall)
h.mux.HandleFunc("/api/service/uninstall", h.handleServiceUninstall)
}
@ -168,6 +170,7 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
Mode string `json:"mode"`
Name string `json:"name"`
WebPort int `json:"web_port"`
AllowForceDetach *bool `json:"allow_force_detach,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"})
@ -186,6 +189,9 @@ func (h *Handler) handleConfig(w http.ResponseWriter, r *http.Request) {
if updates.WebPort > 0 {
h.cfg.WebPort = updates.WebPort
}
if updates.AllowForceDetach != nil {
h.cfg.AllowForceDetach = *updates.AllowForceDetach
}
if err := h.cfg.Save(h.cfgPath); err != nil {
writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()})
@ -284,6 +290,34 @@ func (h *Handler) handleAutoConnect(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]interface{}{"ok": true})
}
func (h *Handler) handleForceDetach(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", 405)
return
}
var req struct {
ClientID string `json:"client_id"`
BusID string `json:"bus_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, map[string]interface{}{"ok": false, "error": "invalid request"})
return
}
if h.ForceDetachDevice == nil {
writeJSON(w, map[string]interface{}{"ok": false, "error": "not in use mode"})
return
}
if err := h.ForceDetachDevice(req.ClientID, req.BusID); 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)

View File

@ -147,7 +147,8 @@ function renderUseDevices(container, available, attached) {
</div>
<div class="device-status">
${dev.status === 'in_use'
? '<span class="badge in-use">In Benutzung</span>'
? `<span class="badge in-use">In Benutzung</span>
${dev.allow_force_detach ? `<button class="btn small danger" onclick="forceDetach('${clientId}', '${dev.bus_id}')">Trennen</button>` : ''}`
: isAttached
? '<span class="badge attached">Verbunden</span>'
: `<span class="badge available">Verfuegbar</span>
@ -197,6 +198,24 @@ async function detachDevice(clientId, busId) {
}
}
// Force detach
async function forceDetach(clientId, busId) {
try {
const resp = await fetch(API_BASE + '/api/force-detach', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: clientId, bus_id: busId })
});
const data = await resp.json();
if (!data.ok) {
alert('Fehler: ' + (data.error || 'Unbekannt'));
}
updateDevices();
} catch (e) {
alert('Verbindungsfehler: ' + e.message);
}
}
// Auto-connect toggle
async function toggleAutoConnect(vendorId, productId, enabled) {
try {
@ -226,6 +245,7 @@ async function loadSettings() {
document.getElementById('mode').value = cfg.mode || 'use';
document.getElementById('client-name').value = cfg.name || '';
document.getElementById('web-port').value = cfg.web_port || 8080;
document.getElementById('allow-force-detach').checked = cfg.allow_force_detach || false;
document.getElementById('token1').value = cfg.token1 || '';
document.getElementById('token2').value = cfg.token2 || '';
document.getElementById('token3').value = cfg.token3 || '';
@ -248,6 +268,7 @@ document.getElementById('settings-form').addEventListener('submit', async (e) =>
mode: document.getElementById('mode').value,
name: document.getElementById('client-name').value,
web_port: parseInt(document.getElementById('web-port').value),
allow_force_detach: document.getElementById('allow-force-detach').checked,
})
});
const data = await resp.json();

View File

@ -55,6 +55,13 @@
<label for="web-port">Web-UI Port</label>
<input type="number" id="web-port" value="8080">
</div>
<div class="form-group">
<label class="auto-connect-label">
<input type="checkbox" id="allow-force-detach">
Andere Clients duerfen Geraete trennen (Share-Modus)
</label>
<small>Erlaubt Use-Clients, Geraete die von anderen benutzt werden zu trennen</small>
</div>
<button type="submit" class="btn primary">Speichern</button>
</form>
</section>