Skip to content

Commit 6536077

Browse files
authored
Devicectl (#21)
* Use devicectl to launch and control iphone app
1 parent 22e196a commit 6536077

File tree

9 files changed

+613
-58
lines changed

9 files changed

+613
-58
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
using System.Globalization;
6+
7+
namespace MSTestX.Console.DeviceCtl
8+
{
9+
public partial class DeviceDetails
10+
{
11+
[JsonPropertyName("info")]
12+
public Info Info { get; set; }
13+
14+
[JsonPropertyName("result")]
15+
public Result Result { get; set; }
16+
}
17+
18+
public partial class Info
19+
{
20+
[JsonPropertyName("arguments")]
21+
public string[] Arguments { get; set; }
22+
23+
[JsonPropertyName("commandType")]
24+
public string CommandType { get; set; }
25+
26+
[JsonPropertyName("environment")]
27+
public Environment Environment { get; set; }
28+
29+
[JsonPropertyName("jsonVersion")]
30+
public long JsonVersion { get; set; }
31+
32+
[JsonPropertyName("outcome")]
33+
public string Outcome { get; set; }
34+
35+
[JsonPropertyName("version")]
36+
public string Version { get; set; }
37+
}
38+
39+
public partial class Environment
40+
{
41+
[JsonPropertyName("TERM")]
42+
public string Term { get; set; }
43+
}
44+
45+
public partial class Result
46+
{
47+
[JsonPropertyName("capabilities")]
48+
public Capability[] Capabilities { get; set; }
49+
50+
[JsonPropertyName("connectionProperties")]
51+
public ConnectionProperties ConnectionProperties { get; set; }
52+
53+
[JsonPropertyName("deviceProperties")]
54+
public DeviceProperties DeviceProperties { get; set; }
55+
56+
[JsonPropertyName("hardwareProperties")]
57+
public HardwareProperties HardwareProperties { get; set; }
58+
59+
[JsonPropertyName("identifier")]
60+
public string Identifier { get; set; }
61+
62+
[JsonPropertyName("tags")]
63+
public object[] Tags { get; set; }
64+
65+
[JsonPropertyName("visibilityClass")]
66+
public string VisibilityClass { get; set; }
67+
}
68+
69+
public partial class Capability
70+
{
71+
[JsonPropertyName("featureIdentifier")]
72+
public string FeatureIdentifier { get; set; }
73+
74+
[JsonPropertyName("name")]
75+
public string Name { get; set; }
76+
}
77+
78+
public partial class ConnectionProperties
79+
{
80+
[JsonPropertyName("authenticationType")]
81+
public string AuthenticationType { get; set; }
82+
83+
[JsonPropertyName("isMobileDeviceOnly")]
84+
public bool IsMobileDeviceOnly { get; set; }
85+
86+
[JsonPropertyName("lastConnectionDate")]
87+
public DateTimeOffset LastConnectionDate { get; set; }
88+
89+
[JsonPropertyName("localHostnames")]
90+
public string[] LocalHostnames { get; set; }
91+
92+
[JsonPropertyName("pairingState")]
93+
public string PairingState { get; set; }
94+
95+
[JsonPropertyName("potentialHostnames")]
96+
public string[] PotentialHostnames { get; set; }
97+
98+
[JsonPropertyName("transportType")]
99+
public string TransportType { get; set; }
100+
101+
[JsonPropertyName("tunnelIPAddress")]
102+
public string TunnelIpAddress { get; set; }
103+
104+
[JsonPropertyName("tunnelState")]
105+
public string TunnelState { get; set; }
106+
107+
[JsonPropertyName("tunnelTransportProtocol")]
108+
public string TunnelTransportProtocol { get; set; }
109+
}
110+
111+
public partial class DeviceProperties
112+
{
113+
[JsonPropertyName("bootState")]
114+
public string BootState { get; set; }
115+
116+
[JsonPropertyName("bootedFromSnapshot")]
117+
public bool BootedFromSnapshot { get; set; }
118+
119+
[JsonPropertyName("bootedSnapshotName")]
120+
public string BootedSnapshotName { get; set; }
121+
122+
[JsonPropertyName("ddiServicesAvailable")]
123+
public bool DdiServicesAvailable { get; set; }
124+
125+
[JsonPropertyName("developerModeStatus")]
126+
public string DeveloperModeStatus { get; set; }
127+
128+
[JsonPropertyName("hasInternalOSBuild")]
129+
public bool HasInternalOsBuild { get; set; }
130+
131+
[JsonPropertyName("name")]
132+
public string Name { get; set; }
133+
134+
[JsonPropertyName("osBuildUpdate")]
135+
public string OsBuildUpdate { get; set; }
136+
137+
[JsonPropertyName("osVersionNumber")]
138+
public string OsVersionNumber { get; set; }
139+
140+
[JsonPropertyName("rootFileSystemIsWritable")]
141+
public bool RootFileSystemIsWritable { get; set; }
142+
143+
[JsonPropertyName("screenViewingURL")]
144+
public string ScreenViewingUrl { get; set; }
145+
}
146+
147+
public partial class HardwareProperties
148+
{
149+
[JsonPropertyName("cpuType")]
150+
public CpuType CpuType { get; set; }
151+
152+
[JsonPropertyName("deviceType")]
153+
public string DeviceType { get; set; }
154+
155+
[JsonPropertyName("ecid")]
156+
public long Ecid { get; set; }
157+
158+
[JsonPropertyName("hardwareModel")]
159+
public string HardwareModel { get; set; }
160+
161+
[JsonPropertyName("internalStorageCapacity")]
162+
public long InternalStorageCapacity { get; set; }
163+
164+
[JsonPropertyName("isProductionFused")]
165+
public bool IsProductionFused { get; set; }
166+
167+
[JsonPropertyName("marketingName")]
168+
public string MarketingName { get; set; }
169+
170+
[JsonPropertyName("platform")]
171+
public string Platform { get; set; }
172+
173+
[JsonPropertyName("productType")]
174+
public string ProductType { get; set; }
175+
176+
[JsonPropertyName("reality")]
177+
public string Reality { get; set; }
178+
179+
[JsonPropertyName("serialNumber")]
180+
public string SerialNumber { get; set; }
181+
182+
[JsonPropertyName("supportedCPUTypes")]
183+
public CpuType[] SupportedCpuTypes { get; set; }
184+
185+
[JsonPropertyName("supportedDeviceFamilies")]
186+
public long[] SupportedDeviceFamilies { get; set; }
187+
188+
[JsonPropertyName("thinningProductType")]
189+
public string ThinningProductType { get; set; }
190+
191+
[JsonPropertyName("udid")]
192+
public string Udid { get; set; }
193+
}
194+
195+
public partial class CpuType
196+
{
197+
[JsonPropertyName("name")]
198+
public string Name { get; set; }
199+
200+
[JsonPropertyName("subType")]
201+
public long SubType { get; set; }
202+
203+
[JsonPropertyName("type")]
204+
public long Type { get; set; }
205+
}
206+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
using System.Globalization;
7+
8+
namespace MSTestX.Console.DeviceCtl
9+
{
10+
public partial class Devices
11+
{
12+
[JsonPropertyName("info")]
13+
public Info Info { get; set; }
14+
15+
[JsonPropertyName("result")]
16+
public Result Result { get; set; }
17+
}
18+
19+
public partial class Result
20+
{
21+
[JsonPropertyName("devices")]
22+
public Device[] Devices { get; set; }
23+
}
24+
25+
public partial class Device
26+
{
27+
[JsonPropertyName("capabilities")]
28+
public Capability[] Capabilities { get; set; }
29+
30+
[JsonPropertyName("connectionProperties")]
31+
public ConnectionProperties ConnectionProperties { get; set; }
32+
33+
[JsonPropertyName("deviceProperties")]
34+
public DeviceProperties DeviceProperties { get; set; }
35+
36+
[JsonPropertyName("hardwareProperties")]
37+
public HardwareProperties HardwareProperties { get; set; }
38+
39+
[JsonPropertyName("identifier")]
40+
public string Identifier { get; set; }
41+
42+
[JsonPropertyName("tags")]
43+
public object[] Tags { get; set; }
44+
45+
[JsonPropertyName("visibilityClass")]
46+
public string VisibilityClass { get; set; }
47+
}
48+
}

Automation/MSTestX.Console/MSTestX.Console.csproj

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
<ToolCommandName>mstestx.console</ToolCommandName>
1212
<PackageOutputPath>../../nupkg</PackageOutputPath>
1313
<GeneratePackageOnBuild Condition="'$(Configuration)'=='Release'">true</GeneratePackageOnBuild>
14-
<Version>0.33.0</Version>
15-
<LangVersion>7.3</LangVersion>
14+
<Version>0.37.0</Version>
15+
<LangVersion>9.0</LangVersion>
1616
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
1717
<OutputPath>bin\</OutputPath>
1818
<Authors>Morten Nielsen</Authors>
@@ -26,9 +26,13 @@
2626
<PackageTags>MSTest MAUI VSTest Test Unittest</PackageTags>
2727
</PropertyGroup>
2828

29+
<ItemGroup>
30+
<Content Include="mobiledevice">
31+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
32+
</Content>
33+
</ItemGroup>
2934
<ItemGroup>
3035
<PackageReference Include="AndroidXml" Version="1.1.2" />
31-
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
3236
<PackageReference Include="Microsoft.TestPlatform.TestHost" Version="17.1.0" />
3337
<PackageReference Include="Microsoft.TestPlatform.ObjectModel" Version="17.1.0" />
3438
<PackageReference Include="Microsoft.TestPlatform.Extensions.TrxLogger" Version="17.1.0" />
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#nullable enable
2+
using System;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace MSTestX.Console
9+
{
10+
/// <summary>
11+
/// Helper for calling into https://github.com/imkira/mobiledevice
12+
/// </summary>
13+
internal sealed class MobileDevice : IDisposable
14+
{
15+
private Process mobileDeviceProcess;
16+
private TaskCompletionSource? tunnelCompletion;
17+
private MobileDevice()
18+
{
19+
if (!OperatingSystem.IsMacOS())
20+
throw new PlatformNotSupportedException();
21+
var path = Path.Combine(new FileInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).Directory!.FullName, "mobiledevice");
22+
Process.Start(new ProcessStartInfo("chmod", "+x " + path));
23+
mobileDeviceProcess = new Process() { StartInfo = new ProcessStartInfo(path) { RedirectStandardOutput = true, RedirectStandardInput = true, RedirectStandardError = true, UseShellExecute = false } };
24+
mobileDeviceProcess.EnableRaisingEvents = true;
25+
mobileDeviceProcess.OutputDataReceived += MobileDeviceProcess_OutputDataReceived;
26+
mobileDeviceProcess.Exited += MobileDeviceProcess_Exited;
27+
}
28+
string? lastMessage = null;
29+
private void MobileDeviceProcess_OutputDataReceived(object? sender, DataReceivedEventArgs e)
30+
{
31+
if (e.Data is null) return;
32+
lastMessage = e.Data;
33+
System.Console.WriteLine("mobiledevice: " + e.Data);
34+
if (e.Data.Contains("Tunneling from local port") && tunnelCompletion is not null)
35+
{
36+
tunnelCompletion.TrySetResult();
37+
}
38+
if (e.Data.Contains("Failed to set up port forwarding") && tunnelCompletion is not null)
39+
{
40+
tunnelCompletion.TrySetException(new Exception(e.Data));
41+
}
42+
}
43+
44+
private void MobileDeviceProcess_Exited(object? sender, EventArgs e)
45+
{
46+
mobileDeviceProcess = null!;
47+
Task.Delay(1).ContinueWith(t =>
48+
{
49+
tunnelCompletion?.TrySetException(new Exception("MobileDevice exited unexpectedly with code " + mobileDeviceProcess.ExitCode + (string.IsNullOrEmpty(lastMessage) ? "" : ". " + lastMessage)));
50+
});
51+
Exited?.Invoke(this, mobileDeviceProcess.ExitCode);
52+
}
53+
54+
private async Task StartTunnelAsync(int fromPort, int toPort, string uuid)
55+
{
56+
Process.Start("pkill", "mobiledevice"); // Ensure mobiledevice isn't already running
57+
await Task.Delay(100);
58+
tunnelCompletion = new TaskCompletionSource();
59+
mobileDeviceProcess.StartInfo.Arguments = $"tunnel -u {uuid} {fromPort} {toPort}";
60+
CancellationTokenSource tcs = new CancellationTokenSource();
61+
tcs.CancelAfter(5000);
62+
tcs.Token.Register(() => tunnelCompletion.TrySetException(new TimeoutException()));
63+
mobileDeviceProcess.Start();
64+
mobileDeviceProcess.BeginOutputReadLine();
65+
await tunnelCompletion.Task;
66+
}
67+
68+
public static async Task<MobileDevice> CreateTunnelAsync(int fromPort, int toPort, string uuid)
69+
{
70+
var mobileDevice = new MobileDevice();
71+
await mobileDevice.StartTunnelAsync(fromPort, toPort, uuid);
72+
return mobileDevice;
73+
}
74+
75+
public void Dispose()
76+
{
77+
if (mobileDeviceProcess != null && !mobileDeviceProcess.HasExited)
78+
{
79+
mobileDeviceProcess.Kill(true);
80+
}
81+
mobileDeviceProcess?.Dispose();
82+
mobileDeviceProcess = null!;
83+
}
84+
85+
~MobileDevice()
86+
{
87+
Dispose();
88+
}
89+
90+
public event EventHandler<int>? Exited;
91+
}
92+
}

0 commit comments

Comments
 (0)