Skip to content

Commit

Permalink
Connect to usbmux and scan for devices
Browse files Browse the repository at this point in the history
  • Loading branch information
pimterry committed May 17, 2024
1 parent 9336be5 commit b276b12
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 4 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@
},
"homepage": "https://github.com/httptoolkit/usbmux-client#readme",
"devDependencies": {
"@httptoolkit/util": "^0.1.2",
"@types/chai": "^4.3.16",
"@types/mocha": "^10.0.6",
"@types/plist": "^3.0.5",
"chai": "^4.4.1",
"mocha": "^10.4.0",
"rimraf": "^5.0.7",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
},
"dependencies": {
"plist": "^3.1.0"
}
}
133 changes: 132 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,132 @@
export const PLACEHOLDER = true;
import * as net from 'net';
import * as plist from 'plist';

const DEFAULT_ADDRESS = process.platform === 'win32'
? { port: 27015 }
: { path: '/var/run/usbmuxd' };

export async function getUsbmuxClient(
connectionOptions: net.NetConnectOpts = DEFAULT_ADDRESS
) {
const conn = net.connect(connectionOptions);

await new Promise((resolve, reject) => {
conn.on('connect', resolve);
conn.on('error', reject);
});

// Start listening for connected devices:
conn.write(plistSerialize({
MessageType: 'Listen',
ClientVersionString: 'usbmux-client',
ProgName: 'usbmux-client'
}));

const response = await readMessageFromStream(conn);
if (
response === null ||
response.MessageType !== 'Result' ||
response.Number !== 0
) {
throw new Error('Usbmux connection failed');
};

return new UsbmuxClient(conn);
}

function plistSerialize(value: any) {
const plistString = plist.build(value)
const plistBuffer = Buffer.from(plistString, 'utf8');

const length = 16 + plistBuffer.byteLength; // Header always 16 bytes
const version = 0; // Also called 'reserved'? Always 0
const messageType = 8; // 8 is 'plist' message type
const tag = 1; // Echoed in responses, not used for now

const messageHeader = Buffer.alloc(16);
messageHeader.writeUInt32LE(length, 0);
messageHeader.writeUInt32LE(version, 4);
messageHeader.writeUInt32LE(messageType, 8);
messageHeader.writeUInt32LE(tag, 12);

return Buffer.concat([messageHeader, plistBuffer], length);
}

type ResultMessage = { MessageType: 'Result', Number: number };
type AttachedMessage = { MessageType: 'Attached', DeviceID: number, Properties: Record<string, string | number> };
type DetachedMessage = { MessageType: 'Detached', DeviceID: number };

type ResponseMessage =
| ResultMessage
| AttachedMessage
| DetachedMessage;

const readMessageFromStream = async (stream: net.Socket): Promise<ResponseMessage | null> => {
if (stream.closed) return null;

const header = stream.read(16);
if (!header) {
await new Promise((resolve, reject) => {
stream.once('readable', resolve);
stream.once('close', resolve);
stream.once('error', reject);
});
return readMessageFromStream(stream);
}

const payloadLength = header.readUInt32LE(0) - 16; // Minus the header length

const payload = stream.read(payloadLength);
if (!payload) {
stream.unshift(header);
await new Promise((resolve, reject) => {
stream.once('readable', resolve);
stream.once('close', resolve);
stream.once('error', reject);
});
return readMessageFromStream(stream);
}

return plist.parse(payload.toString('utf8')) as ResponseMessage;
}

class UsbmuxClient {

constructor(
private socket: net.Socket
) {
this.listenToMessages(socket)
}

private deviceData: Record<number, Record<string, string | number>> = {};

// Listen for events by using readMessageFromStream in an async iterator:
async listenToMessages(socket: net.Socket) {
while (true) {
const message = await readMessageFromStream(socket);
if (message === null) {
this.close();
return;
}

if (message.MessageType === 'Attached') {
this.deviceData[message.DeviceID] = message.Properties;
} else if (message.MessageType === 'Detached') {
delete this.deviceData[message.DeviceID];

}
}
}

getDevices() {
return this.deviceData;
}

close() {
this.socket.end();
this.deviceData = {};
}

}

export type { UsbmuxClient };
22 changes: 19 additions & 3 deletions test/test.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import { expect } from "chai";
import { delay } from "@httptoolkit/util";

import { PLACEHOLDER } from "../src/index";
import { UsbmuxClient, getUsbmuxClient } from "../src/index";

describe("Usbmux-client", () => {

it("placeholder test", () => {
expect(PLACEHOLDER).to.equal(true);
let client: UsbmuxClient | undefined;

afterEach(() => {
client?.close();
client = undefined;
})

it("can expose the connected devices ", async () => {
client = await getUsbmuxClient();
await delay(10); // Not clear this is necessary, but not unhelpful

const devices = client.getDevices();

// Tests assume no devices (since this will always be the state in
// the CI environment) but this at least confirms we can connect
// to Usbmux successfully.
expect(devices).to.deep.equal({});
});

});

0 comments on commit b276b12

Please sign in to comment.