Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions Xamarin.MacDev/DeviceCtlOutputParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Text.Json;
using Xamarin.MacDev.Models;

#nullable enable

namespace Xamarin.MacDev;

Comment on lines +11 to +12
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

This file uses a file-scoped namespace (namespace Xamarin.MacDev;), while existing library files use block-scoped namespaces (namespace Xamarin.MacDev { ... }). For consistency, please switch this file to the block-scoped namespace style.

Copilot uses AI. Check for mistakes.
/// <summary>
/// Pure parsing of <c>xcrun devicectl list devices</c> JSON output into model objects.
/// JSON structure follows Apple's devicectl output format, validated against
/// parsing patterns from dotnet/macios GetAvailableDevices task.
/// </summary>
public static class DeviceCtlOutputParser {
Comment on lines +13 to +18
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

The PR description focuses on simulator management (simctl), but this PR also introduces a new devicectl parser + physical-device model/tests. Please update the PR description (and/or the linked issue/closure) to reflect these additional changes, or split them into a separate PR if they’re out of scope.

Copilot uses AI. Check for mistakes.

static readonly JsonDocumentOptions JsonOptions = new JsonDocumentOptions {
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip,
};

/// <summary>
/// Parses the JSON output of <c>xcrun devicectl list devices</c>
/// into a list of <see cref="PhysicalDeviceInfo"/>.
/// </summary>
public static List<PhysicalDeviceInfo> ParseDevices (string? json, ICustomLogger? log = null)
{
var devices = new List<PhysicalDeviceInfo> ();
if (string.IsNullOrEmpty (json))
return devices;

try {
using (var doc = JsonDocument.Parse (json!, JsonOptions)) {
// Navigate to result.devices array
if (!doc.RootElement.TryGetProperty ("result", out var result))
return devices;
if (!result.TryGetProperty ("devices", out var devicesArray))
return devices;
if (devicesArray.ValueKind != JsonValueKind.Array)
return devices;

foreach (var device in devicesArray.EnumerateArray ()) {
var info = new PhysicalDeviceInfo {
Identifier = GetString (device, "identifier"),
};

// deviceProperties
if (device.TryGetProperty ("deviceProperties", out var deviceProps)) {
info.Name = GetString (deviceProps, "name");
info.BuildVersion = GetString (deviceProps, "osBuildUpdate");
info.OSVersion = GetString (deviceProps, "osVersionNumber");
}

// hardwareProperties
if (device.TryGetProperty ("hardwareProperties", out var hwProps)) {
info.Udid = GetString (hwProps, "udid");
info.DeviceClass = GetString (hwProps, "deviceType");
info.HardwareModel = GetString (hwProps, "hardwareModel");
info.Platform = GetString (hwProps, "platform");
info.ProductType = GetString (hwProps, "productType");
info.SerialNumber = GetString (hwProps, "serialNumber");

if (hwProps.TryGetProperty ("ecid", out var ecidElement)) {
if (ecidElement.TryGetUInt64 (out var ecid))
info.UniqueChipID = ecid;
}

// cpuType.name
if (hwProps.TryGetProperty ("cpuType", out var cpuType))
info.CpuArchitecture = GetString (cpuType, "name");
}

// connectionProperties
if (device.TryGetProperty ("connectionProperties", out var connProps)) {
info.TransportType = GetString (connProps, "transportType");
info.PairingState = GetString (connProps, "pairingState");
}

// Fallback: use identifier as UDID if hardware UDID is missing
if (string.IsNullOrEmpty (info.Udid))
info.Udid = info.Identifier;

devices.Add (info);
}
}
} catch (JsonException ex) {
log?.LogInfo ("DeviceCtlOutputParser.ParseDevices failed: {0}", ex.Message);
} catch (InvalidOperationException ex) {
log?.LogInfo ("DeviceCtlOutputParser.ParseDevices failed: {0}", ex.Message);
}

return devices;
}

static string GetString (JsonElement element, string property)
{
if (element.TryGetProperty (property, out var value)) {
if (value.ValueKind == JsonValueKind.Null || value.ValueKind == JsonValueKind.Undefined)
return "";
if (value.ValueKind == JsonValueKind.String)
return value.GetString () ?? "";
return value.ToString ();
}
return "";
}
}
56 changes: 56 additions & 0 deletions Xamarin.MacDev/Models/PhysicalDeviceInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable

namespace Xamarin.MacDev.Models;

/// <summary>
/// Information about a physical Apple device from xcrun devicectl.
/// Corresponds to entries in the "result.devices" section of devicectl JSON.
/// </summary>
public class PhysicalDeviceInfo {
/// <summary>The device display name (e.g. "Rolf's iPhone 15").</summary>
public string Name { get; set; } = "";

/// <summary>The device UDID.</summary>
public string Udid { get; set; } = "";

/// <summary>The device identifier (GUID from devicectl).</summary>
public string Identifier { get; set; } = "";

/// <summary>The OS build version (e.g. "23B85").</summary>
public string BuildVersion { get; set; } = "";

/// <summary>The OS version number (e.g. "18.1").</summary>
public string OSVersion { get; set; } = "";

/// <summary>The device class (e.g. "iPhone", "iPad", "appleWatch").</summary>
public string DeviceClass { get; set; } = "";

/// <summary>The hardware model (e.g. "D83AP").</summary>
public string HardwareModel { get; set; } = "";

/// <summary>The platform (e.g. "iOS", "watchOS").</summary>
public string Platform { get; set; } = "";

/// <summary>The product type (e.g. "iPhone16,1").</summary>
public string ProductType { get; set; } = "";

/// <summary>The serial number.</summary>
public string SerialNumber { get; set; } = "";

/// <summary>The ECID (unique chip identifier).</summary>
public ulong? UniqueChipID { get; set; }

/// <summary>The CPU architecture (e.g. "arm64e").</summary>
public string CpuArchitecture { get; set; } = "";

/// <summary>The connection transport type (e.g. "localNetwork").</summary>
public string TransportType { get; set; } = "";

/// <summary>The pairing state (e.g. "paired").</summary>
public string PairingState { get; set; } = "";

public override string ToString () => $"{Name} ({Udid}) [{DeviceClass}]";
Comment on lines +6 to +55
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

This model type uses a file-scoped namespace (namespace Xamarin.MacDev.Models;), but existing files in Xamarin.MacDev/Models use block-scoped namespaces (namespace Xamarin.MacDev.Models { ... }). Please convert this file to match the established style.

Suggested change
namespace Xamarin.MacDev.Models;
/// <summary>
/// Information about a physical Apple device from xcrun devicectl.
/// Corresponds to entries in the "result.devices" section of devicectl JSON.
/// </summary>
public class PhysicalDeviceInfo {
/// <summary>The device display name (e.g. "Rolf's iPhone 15").</summary>
public string Name { get; set; } = "";
/// <summary>The device UDID.</summary>
public string Udid { get; set; } = "";
/// <summary>The device identifier (GUID from devicectl).</summary>
public string Identifier { get; set; } = "";
/// <summary>The OS build version (e.g. "23B85").</summary>
public string BuildVersion { get; set; } = "";
/// <summary>The OS version number (e.g. "18.1").</summary>
public string OSVersion { get; set; } = "";
/// <summary>The device class (e.g. "iPhone", "iPad", "appleWatch").</summary>
public string DeviceClass { get; set; } = "";
/// <summary>The hardware model (e.g. "D83AP").</summary>
public string HardwareModel { get; set; } = "";
/// <summary>The platform (e.g. "iOS", "watchOS").</summary>
public string Platform { get; set; } = "";
/// <summary>The product type (e.g. "iPhone16,1").</summary>
public string ProductType { get; set; } = "";
/// <summary>The serial number.</summary>
public string SerialNumber { get; set; } = "";
/// <summary>The ECID (unique chip identifier).</summary>
public ulong? UniqueChipID { get; set; }
/// <summary>The CPU architecture (e.g. "arm64e").</summary>
public string CpuArchitecture { get; set; } = "";
/// <summary>The connection transport type (e.g. "localNetwork").</summary>
public string TransportType { get; set; } = "";
/// <summary>The pairing state (e.g. "paired").</summary>
public string PairingState { get; set; } = "";
public override string ToString () => $"{Name} ({Udid}) [{DeviceClass}]";
namespace Xamarin.MacDev.Models {
/// <summary>
/// Information about a physical Apple device from xcrun devicectl.
/// Corresponds to entries in the "result.devices" section of devicectl JSON.
/// </summary>
public class PhysicalDeviceInfo {
/// <summary>The device display name (e.g. "Rolf's iPhone 15").</summary>
public string Name { get; set; } = "";
/// <summary>The device UDID.</summary>
public string Udid { get; set; } = "";
/// <summary>The device identifier (GUID from devicectl).</summary>
public string Identifier { get; set; } = "";
/// <summary>The OS build version (e.g. "23B85").</summary>
public string BuildVersion { get; set; } = "";
/// <summary>The OS version number (e.g. "18.1").</summary>
public string OSVersion { get; set; } = "";
/// <summary>The device class (e.g. "iPhone", "iPad", "appleWatch").</summary>
public string DeviceClass { get; set; } = "";
/// <summary>The hardware model (e.g. "D83AP").</summary>
public string HardwareModel { get; set; } = "";
/// <summary>The platform (e.g. "iOS", "watchOS").</summary>
public string Platform { get; set; } = "";
/// <summary>The product type (e.g. "iPhone16,1").</summary>
public string ProductType { get; set; } = "";
/// <summary>The serial number.</summary>
public string SerialNumber { get; set; } = "";
/// <summary>The ECID (unique chip identifier).</summary>
public ulong? UniqueChipID { get; set; }
/// <summary>The CPU architecture (e.g. "arm64e").</summary>
public string CpuArchitecture { get; set; } = "";
/// <summary>The connection transport type (e.g. "localNetwork").</summary>
public string TransportType { get; set; } = "";
/// <summary>The pairing state (e.g. "paired").</summary>
public string PairingState { get; set; } = "";
public override string ToString () => $"{Name} ({Udid}) [{DeviceClass}]";
}

Copilot uses AI. Check for mistakes.
}
10 changes: 10 additions & 0 deletions Xamarin.MacDev/Models/SimulatorDeviceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Xamarin.MacDev.Models {

/// <summary>
/// Information about a simulator device from xcrun simctl.
/// Fields aligned with dotnet/macios GetAvailableDevices task.
/// </summary>
public class SimulatorDeviceInfo {
/// <summary>The simulator display name (e.g. "iPhone 16 Pro").</summary>
Expand All @@ -27,6 +28,15 @@ public class SimulatorDeviceInfo {
/// <summary>Whether this simulator is available.</summary>
public bool IsAvailable { get; set; }

/// <summary>Availability error message when IsAvailable is false.</summary>
public string AvailabilityError { get; set; } = "";

/// <summary>The runtime version for this device (e.g. "18.2"), derived from the runtime.</summary>
public string OSVersion { get; set; } = "";

/// <summary>The platform (e.g. "iOS", "tvOS"), derived from the runtime identifier.</summary>
public string Platform { get; set; } = "";

public bool IsBooted => State == "Booted";

public override string ToString () => $"{Name} ({Udid}) [{State}]";
Expand Down
32 changes: 32 additions & 0 deletions Xamarin.MacDev/Models/SimulatorDeviceTypeInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#nullable enable

namespace Xamarin.MacDev.Models;

/// <summary>
/// Information about a simulator device type from xcrun simctl.
/// Corresponds to entries in the "devicetypes" section of simctl JSON.
/// </summary>
public class SimulatorDeviceTypeInfo {
/// <summary>The device type identifier (e.g. "com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro").</summary>
public string Identifier { get; set; } = "";

/// <summary>The display name (e.g. "iPhone 16 Pro").</summary>
public string Name { get; set; } = "";

/// <summary>The product family (e.g. "iPhone", "iPad", "Apple TV").</summary>
public string ProductFamily { get; set; } = "";

/// <summary>The minimum runtime version string (e.g. "13.0.0").</summary>
public string MinRuntimeVersionString { get; set; } = "";

/// <summary>The maximum runtime version string (e.g. "65535.255.255").</summary>
public string MaxRuntimeVersionString { get; set; } = "";

/// <summary>The model identifier (e.g. "iPhone12,1").</summary>
public string ModelIdentifier { get; set; } = "";

public override string ToString () => $"{Name} ({Identifier})";
Comment on lines +6 to +31
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

These model types are using file-scoped namespaces (namespace Xamarin.MacDev.Models;), but existing model types in this repo use block-scoped namespaces (namespace Xamarin.MacDev.Models { ... }). Please align to the existing model style for consistency.

Suggested change
namespace Xamarin.MacDev.Models;
/// <summary>
/// Information about a simulator device type from xcrun simctl.
/// Corresponds to entries in the "devicetypes" section of simctl JSON.
/// </summary>
public class SimulatorDeviceTypeInfo {
/// <summary>The device type identifier (e.g. "com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro").</summary>
public string Identifier { get; set; } = "";
/// <summary>The display name (e.g. "iPhone 16 Pro").</summary>
public string Name { get; set; } = "";
/// <summary>The product family (e.g. "iPhone", "iPad", "Apple TV").</summary>
public string ProductFamily { get; set; } = "";
/// <summary>The minimum runtime version string (e.g. "13.0.0").</summary>
public string MinRuntimeVersionString { get; set; } = "";
/// <summary>The maximum runtime version string (e.g. "65535.255.255").</summary>
public string MaxRuntimeVersionString { get; set; } = "";
/// <summary>The model identifier (e.g. "iPhone12,1").</summary>
public string ModelIdentifier { get; set; } = "";
public override string ToString () => $"{Name} ({Identifier})";
namespace Xamarin.MacDev.Models {
/// <summary>
/// Information about a simulator device type from xcrun simctl.
/// Corresponds to entries in the "devicetypes" section of simctl JSON.
/// </summary>
public class SimulatorDeviceTypeInfo {
/// <summary>The device type identifier (e.g. "com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro").</summary>
public string Identifier { get; set; } = "";
/// <summary>The display name (e.g. "iPhone 16 Pro").</summary>
public string Name { get; set; } = "";
/// <summary>The product family (e.g. "iPhone", "iPad", "Apple TV").</summary>
public string ProductFamily { get; set; } = "";
/// <summary>The minimum runtime version string (e.g. "13.0.0").</summary>
public string MinRuntimeVersionString { get; set; } = "";
/// <summary>The maximum runtime version string (e.g. "65535.255.255").</summary>
public string MaxRuntimeVersionString { get; set; } = "";
/// <summary>The model identifier (e.g. "iPhone12,1").</summary>
public string ModelIdentifier { get; set; } = "";
public override string ToString () => $"{Name} ({Identifier})";
}

Copilot uses AI. Check for mistakes.
}
3 changes: 3 additions & 0 deletions Xamarin.MacDev/Models/SimulatorRuntimeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public class SimulatorRuntimeInfo {
/// <summary>Whether this runtime is bundled with Xcode (vs downloaded separately).</summary>
public bool IsBundled { get; set; }

/// <summary>CPU architectures supported by this runtime (e.g. "arm64", "x86_64").</summary>
public System.Collections.Generic.List<string> SupportedArchitectures { get; set; } = new System.Collections.Generic.List<string> ();

Comment on lines +33 to +35
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

This property uses fully-qualified System.Collections.Generic.List<string> in a file that otherwise doesn’t use fully-qualified type names. For readability/consistency with other model types (e.g. EnvironmentCheckResult.cs uses using System.Collections.Generic; + List<T>), consider adding the using and switching this to List<string>.

Copilot uses AI. Check for mistakes.
public override string ToString () => $"{Name} ({Identifier})";
}
}
60 changes: 60 additions & 0 deletions Xamarin.MacDev/SimCtl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.IO;

#nullable enable

namespace Xamarin.MacDev;

/// <summary>
/// Low-level wrapper for running <c>xcrun simctl</c> subcommands.
/// Shared by <see cref="SimulatorService"/> and <c>RuntimeService</c>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

The XML doc comment mentions RuntimeService, but there is no such type in this repository. This makes the documentation misleading; please either remove the reference or update it to the correct consumer type(s).

Suggested change
/// Shared by <see cref="SimulatorService"/> and <c>RuntimeService</c>
/// Shared by <see cref="SimulatorService"/> and other higher-level services

Copilot uses AI. Check for mistakes.
/// to avoid duplicated subprocess execution logic.
/// Logs all subprocess executions and their results.
/// </summary>
public class SimCtl {

static readonly string XcrunPath = "/usr/bin/xcrun";

readonly ICustomLogger log;

public SimCtl (ICustomLogger log)
{
this.log = log ?? throw new ArgumentNullException (nameof (log));
}

/// <summary>
/// Runs <c>xcrun simctl {args}</c> and returns stdout, or null on failure.
/// All subprocess executions and errors are logged.
/// </summary>
public string? Run (params string [] args)
{
if (!File.Exists (XcrunPath)) {
log.LogInfo ("xcrun not found at '{0}'.", XcrunPath);
return null;
}

var fullArgs = new string [args.Length + 1];
fullArgs [0] = "simctl";
Array.Copy (args, 0, fullArgs, 1, args.Length);

log.LogInfo ("Executing: {0} {1}", XcrunPath, string.Join (" ", fullArgs));

try {
var (exitCode, stdout, stderr) = ProcessUtils.Exec (XcrunPath, fullArgs);
if (exitCode != 0) {
log.LogInfo ("simctl {0} failed (exit {1}): {2}", args.Length > 0 ? args [0] : "", exitCode, stderr.Trim ());
return null;
}
return stdout;
} catch (System.ComponentModel.Win32Exception ex) {
log.LogInfo ("Could not run xcrun simctl: {0}", ex.Message);
return null;
} catch (InvalidOperationException ex) {
log.LogInfo ("Could not run xcrun simctl: {0}", ex.Message);
return null;
Comment on lines +9 to +57
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

This file uses a file-scoped namespace (namespace Xamarin.MacDev;), but the repository convention in existing files is namespace Xamarin.MacDev { ... } (see Xamarin.MacDev/ProcessUtils.cs). Please align this file to the block-scoped namespace style.

Suggested change
namespace Xamarin.MacDev;
/// <summary>
/// Low-level wrapper for running <c>xcrun simctl</c> subcommands.
/// Shared by <see cref="SimulatorService"/> and <c>RuntimeService</c>
/// to avoid duplicated subprocess execution logic.
/// Logs all subprocess executions and their results.
/// </summary>
public class SimCtl {
static readonly string XcrunPath = "/usr/bin/xcrun";
readonly ICustomLogger log;
public SimCtl (ICustomLogger log)
{
this.log = log ?? throw new ArgumentNullException (nameof (log));
}
/// <summary>
/// Runs <c>xcrun simctl {args}</c> and returns stdout, or null on failure.
/// All subprocess executions and errors are logged.
/// </summary>
public string? Run (params string [] args)
{
if (!File.Exists (XcrunPath)) {
log.LogInfo ("xcrun not found at '{0}'.", XcrunPath);
return null;
}
var fullArgs = new string [args.Length + 1];
fullArgs [0] = "simctl";
Array.Copy (args, 0, fullArgs, 1, args.Length);
log.LogInfo ("Executing: {0} {1}", XcrunPath, string.Join (" ", fullArgs));
try {
var (exitCode, stdout, stderr) = ProcessUtils.Exec (XcrunPath, fullArgs);
if (exitCode != 0) {
log.LogInfo ("simctl {0} failed (exit {1}): {2}", args.Length > 0 ? args [0] : "", exitCode, stderr.Trim ());
return null;
}
return stdout;
} catch (System.ComponentModel.Win32Exception ex) {
log.LogInfo ("Could not run xcrun simctl: {0}", ex.Message);
return null;
} catch (InvalidOperationException ex) {
log.LogInfo ("Could not run xcrun simctl: {0}", ex.Message);
return null;
namespace Xamarin.MacDev {
/// <summary>
/// Low-level wrapper for running <c>xcrun simctl</c> subcommands.
/// Shared by <see cref="SimulatorService"/> and <c>RuntimeService</c>
/// to avoid duplicated subprocess execution logic.
/// Logs all subprocess executions and their results.
/// </summary>
public class SimCtl {
static readonly string XcrunPath = "/usr/bin/xcrun";
readonly ICustomLogger log;
public SimCtl (ICustomLogger log)
{
this.log = log ?? throw new ArgumentNullException (nameof (log));
}
/// <summary>
/// Runs <c>xcrun simctl {args}</c> and returns stdout, or null on failure.
/// All subprocess executions and errors are logged.
/// </summary>
public string? Run (params string [] args)
{
if (!File.Exists (XcrunPath)) {
log.LogInfo ("xcrun not found at '{0}'.", XcrunPath);
return null;
}
var fullArgs = new string [args.Length + 1];
fullArgs [0] = "simctl";
Array.Copy (args, 0, fullArgs, 1, args.Length);
log.LogInfo ("Executing: {0} {1}", XcrunPath, string.Join (" ", fullArgs));
try {
var (exitCode, stdout, stderr) = ProcessUtils.Exec (XcrunPath, fullArgs);
if (exitCode != 0) {
log.LogInfo ("simctl {0} failed (exit {1}): {2}", args.Length > 0 ? args [0] : "", exitCode, stderr.Trim ());
return null;
}
return stdout;
} catch (System.ComponentModel.Win32Exception ex) {
log.LogInfo ("Could not run xcrun simctl: {0}", ex.Message);
return null;
} catch (InvalidOperationException ex) {
log.LogInfo ("Could not run xcrun simctl: {0}", ex.Message);
return null;
}

Copilot uses AI. Check for mistakes.
}
}
}
Loading