Skip to content

Instantly share code, notes, and snippets.

@sen0rxol0
Last active August 11, 2022 23:46
Show Gist options
  • Save sen0rxol0/daae37e7d34d8d0eef47456e70e88e32 to your computer and use it in GitHub Desktop.
Save sen0rxol0/daae37e7d34d8d0eef47456e70e88e32 to your computer and use it in GitHub Desktop.
usbmuxd listen for device
// Modified from source: https://github.com/DeMille/node-usbmux/blob/master/lib/usbmux.js
const net = require('net'),
usbmuxd = { path: '/var/run/usbmuxd' }
devices = {},
protocol = {
payloadListen: `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>MessageType</key>
<string>Listen</string>
<key>ClientVersionString</key>
<string>node-usbmux</string>
<key>ProgName</key>
<string>node-usbmux</string>
</dict>
</plist>`,
/**
* Listen request
* @type {Buffer}
*/
listen() {
const payloadBuf = Buffer.from(this.payloadListen),
headerBuf = Buffer.alloc(16),
header = {
len: payloadBuf.length + 16,
version: 1,
request: 8,
tag: 1
};
headerBuf.fill(0)
headerBuf.writeUInt32LE(header.len, 0);
headerBuf.writeUInt32LE(header.version, 4)
headerBuf.writeUInt32LE(header.request, 8)
headerBuf.writeUInt32LE(header.tag, 12)
return Buffer.concat([headerBuf, payloadBuf])
},
/**
* Creates a function that will parse messages from data events
*
* net.Socket data events sometimes break up the incoming message across
* multiple events, making it necessary to combine them. This parser function
* assembles messages using the length given in the message header and calls
* the onComplete callback as new messages are assembled. Sometime multiple
* messages will be within a single data buffer too.
*
* @param {makeParserCb} resolve - Called as new msgs are assembled
* @return {function} - Parser function
*
* @callback makeParserCb
* @param {object} - msg object converted from plist
*/
makeParser(resolve) {
// Store status (remaining message length & msg text) of partial messages
// across multiple calls to the parse function
let len = undefined
let msg = undefined
/**
* @param {Buffer} data - From a socket's data event
*/
const parse = (data) => {
// Check if this data represents a new incoming message or is part of an
// existing partially completed message
if (!len) {
// The length of the message's body is the total length (the first
// UInt32LE in the header) minus the length of header itself (16)
len = data.readUInt32LE(0) - 16
msg = ''
// If there is data beyond the header then continue adding data to msg
data = data.slice(16)
if (!data.length) return
}
// Add in data until our remaining length is used up
var body = data.slice(0, len)
msg += body
len -= body.length
// If msg is finished, convert plist to obj and run callback
if (len === 0) {
const msgSplited = new String(msg).split("\n")
const msgObj = {}
let dictKey = undefined
msgSplited.forEach((line, i) => {
if (dictKey && (line.indexOf('</dict>') > 0)) {
dictKey = undefined
}
if (line.indexOf('<key>') > 0) {
const key = line.replace("<key>", "").replace("</key>", "").replace("\t", "")
const nextLine = msgSplited[i + 1]
let value = undefined
if (nextLine.indexOf('<dict>') > 0) {
dictKey = key
value = {}
} else if (nextLine.indexOf('<string>') > 0) {
value = nextLine.replace("<string>", "").replace("</string>", "").replace("\t", "")
if (dictKey) {
value = value.replace("\t", "")
}
} else if (nextLine.indexOf('<integer>') > 0) {
value = parseInt(nextLine.replace("<integer>", "").replace("</integer>", "").replace("\t", ""))
}
if (dictKey && !(typeof value === 'object')) {
msgObj[dictKey][key.replace("\t", "")] = value
} else {
msgObj[key] = value
}
}
})
resolve(msgObj)
}
// If there is any data left over that means there is another message
// so we need to run this parse fct again using the rest of the data
data = data.slice(body.length)
if (data.length) parse(data)
}
return parse
}
};
/**
* Connects to usbmuxd and listens for ios devices
*
* This connection stays open, listening as devices are plugged/unplugged and
* cant be upgraded into a tcp tunnel. You have to start a second connection
* with connect() to actually make tunnel.
*
* @return {net.Socket} - Socket with 2 bolted on events, attached & detached:
*
* Fires when devices are plugged in or first found by the listener
* @event net.Socket#attached
* @type {string} - UDID
*
* Fires when devices are unplugged
* @event net.Socket#detached
* @type {string} - UDID
*
* @public
*/
function createListener() {
const connection = net.connect(usbmuxd)
const onDataParsed = (message) => {
// first response always acknowledges / denies the request:
if ((message.MessageType === 'Result') && (message.Number !== 0)) {
connection.emit('error', new Error('Listen failed with error: ', message.Number))
connection.end()
}
// subsequent responses report on connected device status:
if (message.MessageType === 'Attached') {
devices[message.Properties.SerialNumber] = message.Properties
connection.emit('attached', message.Properties.SerialNumber)
}
if (message.MessageType === 'Detached') {
// given message.DeviceID, find matching device and remove it
Object.keys(devices).forEach((key) => {
if (devices[key].DeviceID === message.DeviceID) {
connection.emit('detached', devices[key].SerialNumber)
delete devices[key]
}
})
}
}
connection.on('data', protocol.makeParser(onDataParsed))
process.nextTick(() => {
connection.write(protocol.listen())
})
return connection
}
function listenForDevice(timeout = 1000) {
return new Promise((resolve, reject) => {
const listener = createListener()
const listenerTimeout = setTimeout(() => {
listener.end()
reject(new Error('No devices connected.'))
}, timeout)
listener.on('attached', (udid) => {
listener.end()
clearTimeout(listenerTimeout)
resolve(devices[udid])
})
})
}
listenForDevice().then((device) => {
console.log(device)
}, (err) => {
console.error(err)
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment