Last active
August 11, 2022 23:46
-
-
Save sen0rxol0/daae37e7d34d8d0eef47456e70e88e32 to your computer and use it in GitHub Desktop.
usbmuxd listen for device
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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