Skip to content

Indigo HID transport constructs the HID client before SimulatorKit is loaded — clientClassUnavailable for library consumers #941

Description

@onevcat

Summary

FBSimulatorIndigoHIDTransport.indigo(for:) evaluates its initializer arguments in declaration order:

static func indigo(for simulator: FBSimulator) throws -> FBSimulatorIndigoHIDTransport {
  FBSimulatorIndigoHIDTransport(
    indigoClient: try FBSimulatorIndigoHIDClient(for: simulator.device),  // 1st: objc_lookUpClass("SimulatorKit.SimDeviceLegacyHIDClient")
    indigo: try FBSimulatorIndigoHID(),                                   // 2nd: loads SimulatorKit via xcodeFrameworks loader
    ...

FBSimulatorIndigoHIDClient.init(for:) looks up SimulatorKit.SimDeviceLegacyHIDClient with objc_lookUpClass, but the thing that actually loads SimulatorKit into the process — FBSimulatorControlFrameworkLoader.xcodeFrameworks.loadPrivateFrameworks() inside FBSimulatorIndigoHID.init() — runs after it. Any process that has not already loaded SimulatorKit by other means throws FBSimulatorHIDError.clientClassUnavailable on the entire Indigo path.

This includes the default transport selection: on a simulator where dtuhidd is not active, FBSimulator.defaultHIDTransport resolves to .indigo, so a plain FBSimulatorHID(for: simulator) fails.

Why idb_companion does not hit this

FBIDBCommandExecutor.connectToHID() calls FBSimulatorControlFrameworkLoader.xcodeFrameworks.loadPrivateFrameworks(...) before connectToHID, so inside the companion SimulatorKit is always resident before the client lookup runs. The bug only surfaces for consumers that link FBSimulatorControl directly and follow the documented HID entry point.

Reproduction

main @ c51004c, macOS 26, Xcode 27.0 beta 4 (DEVELOPER_DIR pointing at it), CoreSimulator 1169.1, booted iOS 27.0 simulator, Device Hub closed (no dtuhidd, so the indigo path is selected):

try FBSimulatorControlFrameworkLoader.essentialFrameworks.loadPrivateFrameworks(logger)
let config = FBSimulatorControlConfiguration(deviceSetPath: nil, logger: logger, reporter: nil)
let set = try FBSimulatorControl.withConfiguration(config).set
let simulator = set.simulator(withUDID: udid)!
let hid = try FBSimulatorHID(for: simulator)   // throws clientClassUnavailable

With FBCONTROLCORE_DEBUG_LOGGING=1 the log confirms only CoreSimulator was ever loaded:

CoreSimulator: Already loaded, skipping
Loaded All Private Frameworks [CoreSimulator]
FAIL: clientClassUnavailable(className: "SimulatorKit.SimDeviceLegacyHIDClient")

Forcing transport: .dtuhid works fine in the same process (no SimulatorKit needed), which also confirms the rest of the stack is healthy.

Suggested fix

Load the Xcode frameworks (or at least SimulatorKit) at the top of indigo(for:) before constructing FBSimulatorIndigoHIDClient, e.g.:

static func indigo(for simulator: FBSimulator) throws -> FBSimulatorIndigoHIDTransport {
  try FBSimulatorControlFrameworkLoader.xcodeFrameworks.loadPrivateFrameworks(nil)
  ...

or reorder so FBSimulatorIndigoHID() is constructed first. Consumers can work around it today by pre-loading xcodeFrameworks themselves before creating an FBSimulatorHID.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions