Skip to content

Add PhysicalDeviceService for devicectl-backed physical device discovery - #196

Merged
rmarinho merged 6 commits into
mainfrom
rmarinho-physical-device-discovery
Oct 21, 2026
Merged

rmarinho merged 6 commits into
mainfrom
rmarinho-physical-device-discovery

Conversation

@rmarinho

@rmarinho rmarinho commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Fixes #195

Summary

Adds PhysicalDeviceService, a public, reusable way to discover physical Apple devices through xcrun devicectl list devices, at about the level of SimulatorService. The downstream consumer is dotnet/maui-labs#722.

var result = await new PhysicalDeviceService (logger).ListAsync (cancellationToken);
if (result.IsSuccess) {
	foreach (var device in result.Devices.Where (d => d.Kind == PhysicalDeviceKind.IPhone && d.IsReachable))
		Console.WriteLine ($"{device.Name} {device.HardwareUdid}");
}

How the issue's requirements are met

  • Async, cancellable, bounded. ListAsync (CancellationToken) and ListAsync (TimeSpan, CancellationToken) (default 30 s). Timeout or cancellation terminates devicectl; the net10.0 build also terminates the processes it started that are still running (see Compatibility).
  • Distinct outcomes. PhysicalDeviceDiscoveryResult.Status is Success (possibly empty), UnsupportedHost, ToolUnavailable, ExecutionFailed, TimedOut or InvalidOutput, with ErrorMessage and ExitCode. A devicectl error object is always ExecutionFailed, even if the process exited with code 0 and the document also has a device list. Only invalid arguments and caller cancellation throw. Diagnostics go to ICustomLogger, never stdout. A failed discovery is logged at info level, because it is already in the result and an ICustomLogger warning becomes a build warning when the logger is an MSBuild task, as in dotnet/macios.
  • Choosing the Xcode. By default devicectl runs with the Xcode the environment selects. new PhysicalDeviceService (logger, developerRoot) (for example AppleSdkSettings.DeveloperRoot, or MSBuild's SdkDevPath) sets DEVELOPER_DIR for the devicectl process only. The current process's environment is untouched, so services for different Xcode installations can run concurrently.
  • Hardware UDID vs CoreDevice identifier. New HardwareUdid / HasHardwareUdid. Udid is unchanged and still falls back to Identifier.
  • Multiple transports. Entries for the same device are merged by hardware UDID and Connections lists each transport, best first. Entries without a hardware UDID are never merged.
  • Transport, pairing, reachability. TransportType, PairingState and the raw ConnectionState are preserved. Availability (Unknown / Unavailable / Available / Connected) and IsReachable are derived only from the connection (tunnel) state, so pairing is never treated as connected, and a missing or unrecognized state stays Unknown. Remembered, unreachable devices are included.
  • Identity and metadata. Kind (iPhone, iPad, iPod, Apple Watch, Apple TV, Vision, Mac) is derived from DeviceClass, then ProductType, then Platform, and never from the name. MarketingName, OS version and build, hardware model and CPU architecture are preserved.
  • Docs. README and XML docs cover minimum Xcode and macOS versions, the CoreDevice device-OS caveat, non-macOS and tool-missing behavior, and the read-only guarantee.
  • Hardware-free tests. A protected virtual seam (see dotnet/macios below), JSON fixtures and a fake xcrun script cover the matrix in the issue: empty, single and multiple devices, repeated enumeration, multiple transports, identity and metadata, connection states, new and legacy schemas, malformed output, command failure, missing tooling, timeout, cancellation, and supported and unsupported hosts.

Compatibility

  • Additive. PhysicalDeviceInfo gains members; existing members and ToString are unchanged. DeviceCtlOutputParser.ParseDevices stays lenient and returns what it did before, including simulators, and the existing parser tests pass unmodified. Both parsers keep accepting trailing commas and comments. A new strict TryParseDevices tells an empty list from invalid output. It also rejects an entry without a text identifier, or with a non-text hardware udid, rather than returning a device nobody can address. The test seam adds two public members to maintain: DeviceCtlRunResult and PhysicalDeviceService.ExecuteDeviceCtlAsync.
  • One lenient-parser fix. JsonElement.TryGet* throws on a value of another JSON kind, so a string ecid (or a non-number cpuType) made ParseDevices drop that device and every device after it. Such optional fields are now skipped and the device is kept. I compared the existing members of what ParseDevices returns before and after this PR, on dotnet/macios's devicectl fixtures, real devicectl output and 24 edge cases (empty, malformed, partial). That case is the only difference.
  • Shared behavior change: ProcessUtils.KillProcess now uses Kill (entireProcessTree: true) in the net10.0 build, so it applies to every ProcessUtils caller. Without it, a grandchild holding the stdout pipe keeps a cancelled call hanging until the grandchild exits. netstandard2.0 is unchanged. CreateProcessStartInfo became internal so tests can inspect the command line.
  • Known limit. As in .NET itself, entireProcessTree only reaches descendants while their parent is still running. A wrapper script that backgrounds a child and exits can leave that child behind, and the call then returns TimedOut after the timeout plus a short grace period instead of waiting for it. Real xcrun replaces itself with devicectl, so there is no intermediate process.

Findings worth knowing

  • Xcode 27's devicectl list devices (JSON v5) also lists simulators (reality: simulated, visibilityClass: simulators). The service excludes them; ParseDevices deliberately does not, to keep its behavior.
  • In JSON v5 the connection state is properties.connection.state (legacy: connectionProperties.tunnelState). properties.state is an unrelated object, and the parser does not confuse the two.
  • A legacy iPad (iPad6,3) attached to the Mac is listed as a sparse entry: no name, hardware UDID, OS version or CPU architecture, a pairingState of unsupported and a tunnel state of unavailable. The service reports it with Availability.Unavailable, and Udid falls back to the CoreDevice identifier.
  • A CoreDevice-capable iPhone (iPhone12,1, iOS 27.2) over USB is listed in full, and it shows two more v5 details. properties.hardware.cpuType has only numeric type and subtype and no name, and subtype is written unsigned (18446744071562067970 for arm64e, where the deprecated subType is the signed -2147483646), so the parser decodes the CPU architecture from the numbers. hardwareModel is only in the deprecated hardwareProperties, so it is read from there. devicectl's own _deprecationNotice says hardwareProperties, deviceProperties and connectionProperties "will be removed in a future release", and devicectl --omit-deprecated-fields-in-json already writes the document without them (on the device it removes exactly those three and the notice, and adds nothing). The service does not pass the flag, but it handles that output: the same device, except that HardwareModel is empty (documented on the property and in the README) and the CPU architecture still comes from the numbers.
  • That iPhone was not paired with the Mac yet: pairingState unpaired and connection state disconnected, which devicectl's own table prints as available. The service reports Availability.Available and IsReachable, with PairingState unpaired, so a caller that deploys should check PairingState as well. Paired with the Mac (checked on an iPhone12,1, iOS 27.2, over USB), the same layout reports pairingState paired and connection state connected, and the service reports Availability.Connected. The paired iPhone also writes serialNumber, marketingName and reality to properties.hardware, which the unpaired one did not.
  • With the Command Line Tools selected, xcrun devicectl exits 72 with unable to find utility "devicectl". A developerRoot that does not exist fails with xcrun: error: missing DEVELOPER_DIR path. Both map to ToolUnavailable, and so does xcrun refusing to run until the Xcode license is accepted.

dotnet/macios

dotnet/macios consumes this repository as a submodule. Its GetAvailableDevices MSBuild task runs devicectl itself and, for devicectl, uses only DeviceCtlOutputParser.ParseDevices from this library. That parser's output is unchanged on the task's devicectl fixtures, and those fixtures also parse under the strict parser. XamarinTask.ExecuteAsync sets DEVELOPER_DIR from SdkDevPath for each process it runs, which is what the developerRoot overload is for.

Same test seam. The task's tests subclass it and override protected virtual ExecuteCtlAsync to return canned JSON. PhysicalDeviceService has the equivalent: protected virtual Task<DeviceCtlRunResult> ExecuteDeviceCtlAsync (CancellationToken), the only place the service starts a process. DeviceCtlRunResult is now public, with FromJson (json), Completed (...), NotFound (...) and UnsupportedHost () factories. A test subclass returns DeviceCtlRunResult.FromJson (json) and the service interprets it exactly as it does real output. The README documents the pattern.

Same tests. tests/MaciosParityTests.cs runs the task's devicectl-only tests through the service, the way the task's tests do (a subclass returning canned JSON): DeviceCtl1, DeviceCtl2_Mac, DeviceCtlJsonVersion5 and the devicectl inputs of EmptyJsons. It maps Devices with a test-local port of the task's mapping and sort (tests/MaciosDeviceMapping.cs) and asserts the task's own expectations: three devices plus the discarded Apple Watch, the discarded Mac, the JSON v5 iPhone on iOS 27.0, and no devices for the degenerate inputs. The fixtures (tests/MaciosDeviceCtlFixtures.cs) are identical to the task's, compared character by character with main at 159f7aa0e, except that the non-breaking spaces in one Apple Watch name are written as an escape, because an editor can silently turn them into plain spaces. The tests that involve simctl (SimCtl*, BootedSimulatorsFirst, DeviceCtlDuplicateSimulator and Ctl1*) cover the task's own simulator handling, merging, filtering and sorting, so they are not repeated. The task's DeviceCtlJsonVersion5 test has no [Test] attribute, so it never runs there (checked on main at 159f7aa0e); the parity test runs it.

Adopting it is not part of this PR. The shape would be a protected virtual PhysicalDeviceService CreatePhysicalDeviceService () in the task that defaults to new PhysicalDeviceService (this, SdkDevPath) (the task is an ICustomLogger), RunDeviceCtlAsync calling ListAsync and mapping discovery.Devices as it does today, and the test wrapper overriding the factory with a canned PhysicalDeviceService subclass.

Differences the task would see.

  • Empty or unstructured output ("", {}, [], {"result":{}}) is InvalidOutput rather than an empty list. Devices is empty then, so mapping Devices gives the same result as today. ParseDevices itself still returns an empty list.
  • Simulators are excluded by the service. The task excludes devicectl devices whose UDID matches a simctl one; both can coexist.
  • If devicectl reports the same hardware more than once (for example once per connection path), the service returns one device with several Connections. The task does not de-duplicate, and its sort comment assumes UDIDs are unique. I have not seen devicectl do this on real hardware.
  • A port of the task's mapping discards the legacy iPad described above (Unknown CPU architecture ''). The service lists it, with Availability.Unavailable. It keeps the unpaired iPhone described above (ios-arm64, status Unpaired), and there the lenient ParseDevices the task calls today and the service give the same device. It also keeps the paired iPhone (ios-arm64, status Paired).

Testing

  • dotnet test tests/tests.csproj -c Release -f net10.0: 506 passed, 1 skipped (pre-existing), 0 failed. The baseline was 217 passed.
  • dotnet format whitespace Xamarin.MacDev.sln --verify-no-changes is clean, and the library builds for netstandard2.0 and net10.0 with warnings as errors. The tests also compile for net472, the Windows CI target.
  • Tests that need a POSIX shell are excluded on Windows with [Platform (Exclude = "Win")].
  • With the real xcrun (Xcode 27) and a legacy iPad attached, ListAsync returns Success with that one device (the simulators devicectl also lists are ignored), and the suite's real-xcrun test passes against it.
  • With the real xcrun (Xcode 27) and a CoreDevice-capable iPhone attached (iOS 27.2, USB, not paired), ListAsync returns Success with that device in about 0.3 s and every field filled in as described above. The simulators are ignored, the lenient ParseDevices and the strict TryParseDevices accept the output, and ParseDevices returns the same device as the service. UnpairedIPhoneDocument in the tests is a copy of that layout with made-up identifiers, run through the service and through the port of the task's mapping. The same iPhone listed with --omit-deprecated-fields-in-json also gives the same device through the service (the same status, version, CPU architecture and ios-arm64 mapping), except for the empty HardwareModel. A third test runs the fixture without the deprecated blocks, because a document that has both would hide a fallback to the deprecated data. Breaking the read of properties.connection.state, or the numeric CPU-type decoding, fails only that test.
  • With the real xcrun (Xcode 27) and an iPhone paired with the Mac (iPhone12,1, iOS 27.2, USB; pairingState paired, connection state connected), ListAsync returns Success in about 0.3 s with Availability.Connected and IsReachable, and the serial number, marketing name and reality are filled in. Both parsers accept the output. The same document listed with --omit-deprecated-fields-in-json gives the same device through the service, except for the empty HardwareModel, and the port of the task's mapping keeps it either way (ios-arm64, status Paired). The tests' Device builder writes these fields in the same places (properties.connection and properties.hardware). The service leaves no temporary file behind after a run.
  • With the real xcrun (Xcode 27, no physical devices) and DEVELOPER_DIR set to the Command Line Tools in the environment: the default service reports ToolUnavailable; developerRoot set to the full Xcode reports Success with no devices; a nonexistent developerRoot reports ToolUnavailable; and the calling process's DEVELOPER_DIR is unchanged afterwards.

Review

Reviewed by two other models (Gemini 4 Argon and GPT-6.1 Sol), in four rounds.

  • Discovery service. Their findings led to the developerRoot overload, info-level failure logging, the license mapping, the stricter timeout check and the stricter TryParseDevices. Both then re-reviewed those fixes and found nothing further. The one major finding I did not act on is the descendant-process limit above: it comes from shared ProcessUtils behavior, and the call stays bounded by the timeout plus the grace period.
  • Test seam and macios parity tests. They found that one macios fixture had lost its non-breaking spaces (my first claim that the fixtures were verbatim copies was wrong in that one place; they are now identical, compared character by character), that the parity sort should use macios's own comparison, that DeviceCtlRunResult.NotFound could produce a failure without a message, and that an override which blocks the calling thread is not bounded by the timeout (now documented). I did not add a guard for an override that returns a null task or result, because it already fails loudly and takes a null-forgiving ! to happen in nullable-aware code, nor a Task.Run around the override, because the contract is now documented. Both re-reviewed those fixes and found nothing further.
  • Real-iPhone fixture and tests. Both found no blocking problems, in the fixture and tests and again in the follow-up. GPT-6.1 Sol's coverage note (the real layout has both the v5 properties and the deprecated ones, so a fallback to the deprecated data would go unnoticed) led to the WithoutDeprecatedProperties helper and the third test. Its wording notes (the README sentence on reachability, one assertion message and the HardwareModel docs) were applied.
  • Copilot's review. Copilot found that, with exit code 0, a document with an error object next to a parseable result.devices array (for example {"error":{"code":1000},"result":{"devices":[]}}) was reported as Success, because the error was only looked at when parsing the list failed. That was valid. CreateResult now checks for the error object before it accepts the list. Real devicectl (Xcode 27) writes info and result on success, and error and info (no result, exit code 1) when a command fails, so a successful run is not affected. Five test cases cover it: an error next to an empty list, next to two devices, and the smallest documents (no info, no description). All five fail on the previous code. Both models reviewed the fix and found no blocking problems. I did not act on their one suggestion to make TryParseDevices reject such a document too: only the service calls it, so its remarks and the README now say that it validates the structure and that the service checks the exit code and the error object.

Not verified on real hardware

Behavior was validated with synthetic fixtures modelled on devicectl's v5 and legacy layouts, a fake xcrun script, and runs of the real xcrun devicectl against physical devices: a legacy iPad that CoreDevice reports as unsupported and unavailable, a CoreDevice-capable iPhone that is unpaired and disconnected (which devicectl prints as available), and an iPhone that is paired and connected over USB. That confirms the reading of disconnected as "reachable, not connected" (Availability.Available) and of connected as Availability.Connected on real hardware. Three points still need a check:

  1. Wireless connections. The paired iPhone was connected over USB (wired). Connections over localNetwork are still known only from sample data (a paired Apple Watch over localNetwork reports disconnected). The raw values are always preserved, so callers can apply their own interpretation.
  2. Older devices. The attached legacy iPad is listed, but as a sparse unsupported entry without an OS version. Which OS versions devicectl lists fully is still unconfirmed, so the docs say such devices "may be missing from the list or listed with little information".
  3. Xcode license. An unaccepted license makes xcrun refuse to run any tool and point at sudo xcodebuild -license. That is matched on stderr and reported as ToolUnavailable. The license is already accepted on the machine used here, so the exact message and exit code (69 in the tests) were not reproduced.

rmarinho and others added 4 commits October 20, 2026 13:16
Expose reusable, devicectl-backed discovery of physical Apple devices at about the level of SimulatorService.

- PhysicalDeviceService.ListAsync runs 'xcrun devicectl list devices' with a bounded timeout and cancellation. It returns a PhysicalDeviceDiscoveryResult whose Status distinguishes success (possibly empty), unsupported host, tool unavailable, execution failure, timeout and invalid output.
- PhysicalDeviceInfo gains HardwareUdid/HasHardwareUdid (distinct from the CoreDevice Identifier), MarketingName, Reality, ConnectionState, Availability/IsReachable, IsSimulated, Kind and Connections. Existing members and DeviceCtlOutputParser.ParseDevices behave as before.
- Entries for the same device over several transports are merged by hardware UDID. Simulators, which newer devicectl versions also list, are excluded.
- DeviceCtlOutputParser.TryParseDevices is a strict parser that tells an empty list from invalid output.
- ProcessUtils now kills the whole process tree on cancellation in the net10.0 build, so a cancelled or timed-out command leaves no orphans.
- Hardware-free tests (fixtures, an injectable runner and a fake xcrun script) and README documentation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reviewed by two other models and checked against how dotnet/macios consumes this library (its GetAvailableDevices task).

- Add a PhysicalDeviceService (log, developerRoot) overload. dotnet/macios runs every Apple tool with DEVELOPER_DIR=SdkDevPath; the value is applied to the devicectl child process only, never to the current process.
- Log failures at info level. The failure is already part of the result, and an ICustomLogger warning becomes an MSBuild build warning in macios.
- Report an unaccepted Xcode license as ToolUnavailable instead of ExecutionFailed.
- Report a timeout only when the timeout itself fired, so an unrelated OperationCanceledException from the runner is no longer misreported.
- TryParseDevices rejects an entry without a text identifier, or with a non-text hardware udid, instead of returning a device nobody can address.
- Stop JsonElement.TryGet* from throwing on a wrong value kind in the optional ecid and cpuType fields. A device with such a field is now kept; before, the lenient parser dropped it and every device after it, and the strict parser failed the whole document. This is the only lenient-path behavior change.
- Correct the docs: only processes that are still running are killed on cancellation, and a connection reports its own pairing state.
- Tests: kill leaked helper processes in TearDown and cover all of the above.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
dotnet/macios tests its devicectl handling by overriding a protected
virtual ExecuteCtlAsync on the task and returning canned JSON. Give
PhysicalDeviceService the same seam, so macios can adopt the service and
keep that style of test.

- Make DeviceCtlRunResult public, with FromJson, Completed, NotFound and
  UnsupportedHost factories.
- Add protected virtual PhysicalDeviceService.ExecuteDeviceCtlAsync, the
  only place the service starts a process. The host check moves into its
  default implementation, so an override works on any host. It replaces
  the internal Func-based runner.
- Add tests that run macios's three devicectl fixtures and the devicectl
  inputs of its EmptyJsons cases through the service, and map the result
  with a port of the task's mapping and sort. Add a sanitized legacy-iPad
  document modelled on a real device.
- Document the pattern in the README. Word the docs on duplicate reports
  and on devices CoreDevice cannot manage after what was observed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Spell the non-breaking spaces of the macios DEVICECTL_JSON_1 fixture as an escape, so the fixture and the Apple Watch description assertion match the originals exactly. A plain space had replaced them.
- Sort like macios in the test-local mapping port: no explicit comparers.
- DeviceCtlRunResult.NotFound rejects a null message, and the service reports a default message for a blank one, so a failed discovery always says what went wrong.
- Document that a ExecuteDeviceCtlAsync override must return its task promptly: the timeout bounds the task, not a call that blocks the calling thread.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Validate the service against a real iPhone (Xcode 27, devicectl JSON version 5,
connected over USB, not paired). UnpairedIPhoneDocument is a sanitized copy of
that layout with made-up identifiers.

- The iPhone is listed with all its metadata, reachable (devicectl's
  "disconnected" tunnel state is shown as "available") while PairingState is
  still "unpaired". The macios mapping keeps it as ios-arm64 / Unpaired, and the
  lenient ParseDevices agrees with the service.
- A third test feeds the same document without the deprecated properties, as
  `devicectl list devices --omit-deprecated-fields-in-json` writes it, because
  a document that has both would hide a fallback to the deprecated data. The
  CPU architecture then comes from the numeric type and unsigned subtype, the
  connection state from properties.connection.state, and HardwareModel (only in
  the deprecated hardwareProperties) is empty.
- Document that HardwareModel can be empty, and that a reachable device is not
  necessarily paired.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinho marked this pull request as ready for review October 20, 2026 17:23
Copilot AI balanced review requested due to automatic review settings October 20, 2026 17:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Error handling needs correction, and the new public API and shared process-tree cancellation changes warrant final human review.

1 open issue
What changed in this PR

Adds reusable physical Apple-device discovery to Xamarin.MacDev, complementing SimulatorService and supporting downstream CLI integration.

Changes:

  • Adds asynchronous discovery with cancellation, timeouts, Xcode selection and explicit outcomes.
  • Extends device metadata, merges transports by hardware identity and excludes simulators.
  • Adds documentation, hardware-free tests and dotnet/macios compatibility coverage.
File Description
Xamarin.MacDev/​ProcessUtils.cs Enables process-tree termination on modern .NET.
Xamarin.MacDev/​PhysicalDeviceService.cs Implements discovery and device merging.
Xamarin.MacDev/​Models/​PhysicalDeviceKind.cs Defines device classifications.
Xamarin.MacDev/​Models/​PhysicalDeviceInfo.cs Adds identity and connection metadata.
Xamarin.MacDev/​Models/​PhysicalDeviceDiscoveryStatus.cs Defines discovery outcomes.
Xamarin.MacDev/​Models/​PhysicalDeviceDiscoveryResult.cs Encapsulates devices and diagnostics.
Xamarin.MacDev/​Models/​PhysicalDeviceConnection.cs Represents individual connections.
Xamarin.MacDev/​Models/​PhysicalDeviceAvailability.cs Defines reachability states.
Xamarin.MacDev/​DeviceCtlRunResult.cs Exposes raw execution results.
Xamarin.MacDev/​DeviceCtlOutputParser.cs Adds strict parsing and metadata extraction.
Xamarin.MacDev/​DeviceCtl.cs Runs devicectl and manages temporary output.
tests/​RecordingLogger.cs Captures test diagnostics.
tests/​ProcessUtilsTests.cs Tests process-tree cancellation.
tests/​ProcessTestHelper.cs Supports process lifecycle tests.
tests/​PhysicalDeviceServiceTests.cs Tests discovery, filtering and merging.
tests/​PhysicalDeviceServiceOverrideTests.cs Tests the public override seam.
tests/​PhysicalDeviceServiceFailureTests.cs Covers failures, cancellation and timeouts.
tests/​PhysicalDeviceInfoTests.cs Tests public device models.
tests/​MaciosParityTests.cs Checks downstream compatibility scenarios.
tests/​MaciosDeviceMapping.cs Models downstream device mapping.
tests/​MaciosDeviceCtlFixtures.cs Supplies downstream JSON fixtures.
tests/​FakeDeviceCtl.cs Provides injectable test runners.
tests/​DeviceCtlTests.cs Exercises execution with fake xcrun scripts.
tests/​DeviceCtlRunResultTests.cs Tests execution-result factories.
tests/​DeviceCtlOutputParserTests.cs Extends parser regression coverage.
tests/​DeviceCtlFixtures.cs Supplies synthetic and hardware-derived fixtures.
README.md Documents discovery usage and limitations.

Review effort: Balanced

Comment thread Xamarin.MacDev/PhysicalDeviceService.cs Resolved
PhysicalDeviceService accepted a document that had an error object next to
a parseable result.devices array as Success, because the error was only
looked at when parsing the device list failed. With exit code 0 that
reported {"error":{"code":1000},"result":{"devices":[]}} as an empty list,
and a non-empty array would have been exposed as the devices of a failed
operation.

CreateResult now checks for the error object before it accepts the list and
returns ExecutionFailed with devicectl's message. Real devicectl writes
either info + result (success) or error + info (failure), so a successful
run is not affected.

TryParseDevices still validates structure only; its remarks and the README
now say that the exit code and the error object are checked by the service.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinho requested a review from rolfbjarne October 21, 2026 10:48
@rmarinho
rmarinho merged commit 1396bfc into main Oct 21, 2026
7 checks passed
rmarinho added a commit to dotnet/maui-labs that referenced this pull request Oct 22, 2026
Bump Xamarin.Apple.Tools.MaciOS for PhysicalDeviceService
(dotnet/macios-devtools#196) and report paired, reachable iPhones and iPads
from `xcrun devicectl` next to the simulators, as Android already does for
physical devices. The device id is the hardware UDID, which is stable across
connections, and devicectl runs only for queries that reach the Apple provider.

Startup profiling stays simulator-only, so the profile target resolver skips
physical devices instead of picking one by accident.

Fixes #722

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose reusable physical Apple-device discovery backed by devicectl

3 participants