Multi-instance manager for usbmuxd-win-mce - manages multiple usbmuxd processes with batch device allocation
npm install @mcesystems/usbmuxd-instance-managerusb-device-listener to detect iOS device connections
┌──────────────────────────────────────────────────────────────┐
│ Windows Host │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ usbmuxd-instance-manager (Node.js) │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ usb-device-listener │ │ │
│ │ │ Detects iOS devices (VID: 0x05AC) │ │ │
│ │ └──────────────┬───────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────▼───────────────────────────────────┐ │ │
│ │ │ InstanceManager │ │ │
│ │ │ - Batch allocation (4 devices per instance) │ │ │
│ │ │ - Port assignment (27015, 27016, 27017...) │ │ │
│ │ │ - Spawns via WSL2 │ │ │
│ │ └──────────────┬───────────────────────────────────┘ │ │
│ └─────────────────┼──────────────────────────────────────┘ │
│ │ wsl -d usbmuxd-alpine usbmuxd ... │
│ ┌─────────────────▼──────────────────────────────────────┐ │
│ │ WSL2 - Alpine Linux (26MB) │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ usbmuxd instance │ │ usbmuxd instance │ │ │
│ │ │ Port: 27015 │ │ Port: 27016 │ ... │ │
│ │ │ Devices 1-4 │ │ Devices 5-8 │ │ │
│ │ └──────────────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
`
Installation
`bash
From workspace root
pnpm install
Build the package
pnpm --filter @mcesystems/usbmuxd-instance-manager build
`
Usage
$3
`bash
Run with defaults
pnpm --filter @mcesystems/usbmuxd-instance-manager dev
Or after building
node dist/cli.js
With custom options
node dist/cli.js --batchSize 6 --basePort 27020
`
$3
`typescript
import { UsbmuxdService } from '@mcesystems/usbmuxd-instance-manager';
const service = new UsbmuxdService({
batchSize: 4,
basePort: 27015,
maxInstances: 20,
usbmuxdPath: 'usbmuxd', // Path inside WSL2
wslDistribution: 'usbmuxd-alpine', // Alpine WSL2 distro
verboseLogging: true,
});
// Start monitoring
service.start();
// Get device port mapping
const port = service.getDevicePort('00008030-001234567890402E');
console.log(Device is on port ${port});
// Get statistics
const stats = service.getStats();
console.log(Managing ${stats.deviceCount} devices across ${stats.instanceCount} instances);
// Stop when done
await service.stop();
`
Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| batchSize | number | 4 | Devices per instance |
| basePort | number | 27015 | Starting TCP port |
| maxInstances | number | 20 | Maximum instances |
| usbmuxdPath | string | usbmuxd | Path to usbmuxd inside WSL2 |
| wslDistribution | string | usbmuxd-alpine | Alpine WSL2 distribution name |
| verboseLogging | boolean | true | Enable verbose output |
| appleVendorId | string | '05AC' | Apple vendor ID (hex) |
$3
The manager automatically syncs lockdown (pairing) files between the native Apple/iTunes lockdown directory and Alpine so devices that were already paired on Windows can be used without pairing again in WSL.
The lockdown directory is resolved per platform:
- Windows: C:\ProgramData\Apple\Lockdown (same directory used by iTunes / Apple Mobile Device)
- macOS: /var/db/lockdown
- Linux: /var/lib/lockdown
Sync behavior:
- To Alpine: When a device is assigned, that device's plist ({UDID}.plist) and SystemConfiguration.plist are copied from the Apple lockdown directory to Alpine /var/lib/lockdown.
- From Alpine: When the service stops, plists for all devices that were assigned this session (plus SystemConfiguration.plist if present) are copied from Alpine back to the Apple lockdown directory so they are available for the next run.
Alpine permissions: /var/lib/lockdown is often root-owned. The sync tries a normal copy first; if that fails (e.g. permission denied), it retries with sudo cp. Ensure the WSL user has passwordless sudo for these commands, or that the Alpine image has /var/lib/lockdown writable by the default user. If both fail, a warning is logged and device assignment continues (pairing can still be done manually).
CLI Options
`
Usage: usbmuxd-instance-manager [OPTIONS]
Options:
--batchSize Number of devices per instance (default: 4)
--basePort Base TCP port for first instance (default: 27015)
--maxInstances Maximum number of instances (default: 20)
--usbmuxdPath Path to usbmuxd executable (WSL: default usbmuxd)
--wslDistribution WSL distribution name (default: alpine-usbmuxd-build)
--verbose Enable verbose logging (default: true)
--appleVid Apple Vendor ID in hex (default: 05AC)
-h, --help Show this help message
`
Events
The InstanceManager and UsbmuxdService emit events for monitoring:
`typescript
service.manager.on('instance-started', (instance) => {
console.log(Instance ${instance.id} started on port ${instance.port});
});
service.manager.on('device-assigned', ({ device, instance, mapping }) => {
console.log(Device ${device.deviceId} → Instance ${instance.id} (port ${mapping.port}));
});
service.manager.on('instance-stopped', (instance) => {
console.log(Instance ${instance.id} stopped);
});
`
Example Scenario
$3
`
Instance 1 (port 27015): iPhone-1, iPhone-2, iPhone-3, iPhone-4
Instance 2 (port 27016): iPhone-5, iPhone-6, iPhone-7, iPhone-8
Instance 3 (port 27017): iPhone-9, iPhone-10, iPhone-11, iPhone-12
`
$3
When iPhone-3 disconnects:
- Instance 1 now has 3 devices
- Next device connects → assigned to Instance 1 (has capacity)
When all devices disconnect from Instance 2:
- Instance 2 automatically terminates
- Frees resources
Flow: Working with usbipd and WSL
On Windows, the same physical USB device can be used by either Windows (e.g. iTunes, Apple Mobile Device) or WSL (usbmuxd), not both. usbipd-win is what moves the device between the two.
$3
| State | Who has the device | iTunes / Windows | usbmuxd (WSL) |
|-------|--------------------|------------------|---------------|
| Not shared | Windows | ✓ | ✗ |
| Bound + attached to WSL | WSL | ✗ | ✓ |
| Bound but detached | Neither (held by usbipd) | ✗ | ✗ |
$3
1. WSL must be running (e.g. the manager starts it, or run wsl -d alpine-usbmuxd-build -- echo ok).
2. Bind the device (take it from Windows):
usbipd bind --busid
3. Attach to WSL:
usbipd attach --wsl=
The instance manager does steps 2–3 automatically when it sees an iOS device. You can also use the script:
`powershell
From packages/usbmuxd-instance-manager
.\scripts\attach-device.ps1
`
$3
1. Detach from WSL (stops forwarding to Linux):
usbipd detach --busid
Or detach all:
usbipd detach -a
2. Unbind so Windows gets the device back:
usbipd unbind --busid
Until you unbind, the device stays "Shared (forced)" and Windows/iTunes will not see it.
$3
`powershell
$usbipd = "C:\Program Files\usbipd-win\usbipd.exe"
List devices and their state (Shared / Not shared, Attached / Not attached)
& $usbipd list
Detach from WSL (device still bound to usbipd)
& $usbipd detach --busid 2-5
& $usbipd detach -a
Unbind so Windows can use the device again (e.g. iTunes)
& $usbipd unbind --busid 2-5
`
$3
- Use device with usbmuxd-instance-manager → bind + attach (manager or script does this when you connect a device).
- Use device with iTunes on Windows → detach then unbind for that bus ID; then iTunes will see it.
Requirements
$3
- Node.js: 18+
- Windows: 10/11 with WSL2
- Alpine WSL2 image: usbmuxd-alpine distribution imported
- USB/IP: usbipd-win for USB device forwarding to WSL2
$3
- pnpm: Workspace package manager
- TypeScript: 5.9+
- esbuild: For bundling
Development
`bash
Install dependencies
pnpm install
Run in dev mode (with auto-reload)
pnpm --filter @mcesystems/usbmuxd-instance-manager dev
Build
pnpm --filter @mcesystems/usbmuxd-instance-manager build
Type check
pnpm --filter @mcesystems/usbmuxd-instance-manager check:types
Clean
pnpm --filter @mcesystems/usbmuxd-instance-manager clean
`
Troubleshooting
$3
- Check usbmuxd path: Ensure usbmuxdPath points to valid executable
- Check permissions: Run as Administrator if needed
- Check ports: Ensure ports 27015+ are not in use
$3
- Check USB drivers: iOS devices need WinUSB driver (install via Zadig)
- Check VID: Ensure appleVendorId` is correct ('05AC' for Apple)