1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
//! Supporting types for the `simctl list` subcommand.

use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Stdio;

use super::{Device, Result, Simctl};

/// Indicates the state of a device.
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum DeviceState {
    /// Indicates that the device is booted.
    Booted,

    /// Indicates that the device is shutdown.
    Shutdown,

    /// Indicates that the device is in an unknown state.
    #[serde(other)]
    Unknown,
}

/// Indicates the state of a pair of devices.
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum DevicePairState {
    /// Indicates that this pair is unavailable because one of its components is
    /// unavailable.
    #[serde(rename = "(unavailable)")]
    Unavailable,

    /// Indicates that this pair is active but not connected.
    #[serde(rename = "(active, disconnected)")]
    ActiveDisconnected,

    /// Indicates that this pair is in a state that is not (yet) recognized by
    /// this library.
    #[serde(other)]
    Unknown,
}

/// Information about a device type.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct DeviceType {
    /// Contains the minimum runtime version that this device type supports.
    /// This is relevant for devices that are newer than the oldest runtime that
    /// has been registered with `simctl`.
    #[serde(rename = "minRuntimeVersion")]
    pub min_runtime_version: usize,

    /// Contains the maximum runtime version that this device type supports.
    /// This is relevant for devices that have been deprecated before the newest
    /// runtime that has been registered with `simctl`.
    #[serde(rename = "maxRuntimeVersion")]
    pub max_runtime_version: usize,

    /// Contains a path to the bundle of this device type. This is usually not
    /// relevant to end-users.
    #[serde(rename = "bundlePath")]
    pub bundle_path: PathBuf,

    /// Contains a human-readable name for this device type.
    pub name: String,

    /// Contains a unique identifier for this device type.
    pub identifier: String,

    /// Contains a machine-readable name for the product family of this device
    /// type.
    #[serde(rename = "productFamily")]
    pub product_family: String,
}

/// Information about a runtime.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct Runtime {
    /// Contains a path to the bundle of this runtime. This is usually not
    /// relevant to end-users.
    #[serde(rename = "bundlePath")]
    pub bundle_path: PathBuf,

    /// Contains the build version of this runtime. This is usually not relevant
    /// to end-users.
    #[serde(rename = "buildversion")]
    pub build_version: String,

    /// Contains the root of this runtime. This is usually not relevant to
    /// end-users.
    #[serde(rename = "runtimeRoot")]
    pub runtime_root: PathBuf,

    /// Contains a unique identifier for this runtime.
    pub identifier: String,

    /// Contains a human-readable version string for this runtime.
    pub version: String,

    /// Indicates if this runtime is available. This is false when the runtime
    /// was first created (automatically) with an older version of Xcode that
    /// shipped with an older version of the iOS simulator and after upgrading
    /// to a newer version. In that case, Xcode no longer has the runtime bundle
    /// for this older runtime, but it will still be registered by `simctl`.
    /// However, it's not possible to boot a device with an unavailable runtime.
    #[serde(rename = "isAvailable")]
    pub is_available: bool,

    /// Contains a human-readable name for this runtime.
    pub name: String,
}

/// Information about a device.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct DeviceInfo {
    /// Note: this field is not directly present in JSON. Instead, the JSON
    /// representation is a hashmap of runtime IDs (keys) and devices (values)
    /// that we later connect during deserialization.
    #[serde(skip_deserializing)]
    pub runtime_identifier: String,

    /// If this device is not available (see [`DeviceInfo::is_available`]), this
    /// will contain a (slightly) more detailed explanation for its
    /// unavailability.
    #[serde(default, rename = "availabilityError")]
    pub availability_error: Option<String>,

    /// Contains the path where application data is stored.
    #[serde(rename = "dataPath")]
    pub data_path: PathBuf,

    /// Contains the path where logs are written to.
    #[serde(rename = "logPath")]
    pub log_path: PathBuf,

    /// Contains a unique identifier for this device.
    pub udid: String,

    /// Indicates if this device is available. Also see
    /// [`Runtime::is_available`].
    #[serde(rename = "isAvailable")]
    pub is_available: bool,

    /// This corresponds to [`DeviceType::identifier`]. This is missing for
    /// devices whose device type has since been removed from Xcode.
    #[serde(default, rename = "deviceTypeIdentifier")]
    pub device_type_identifier: String,

    /// Contains the state of this device.
    pub state: DeviceState,

    /// Contains the name of this device.
    pub name: String,
}

/// Short summary of a device that is used as part of a device pair.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct DeviceSummary {
    /// Contains the name of this device.
    pub name: String,

    /// Contains a unique identifier for this device.
    pub udid: String,

    /// Contains the state of this device.
    pub state: DeviceState,
}

/// Information about a device pair.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct DevicePair {
    /// Note: this field is not directly present in JSON. Instead, the JSON
    /// representation is a hashmap of runtime IDs (keys) and devices (values)
    /// that we later connect during deserialization.
    #[serde(skip_deserializing)]
    pub udid: String,

    /// Contains a summary of the watch device.
    pub watch: DeviceSummary,

    /// Contains a summary of the phone device.
    pub phone: DeviceSummary,

    /// Contains the state of this device pair.
    pub state: DevicePairState,
}

/// Wrapper around the `simctl list` subcommand's output.
#[derive(Debug)]
pub struct List {
    simctl: Simctl,
    device_types: Vec<DeviceType>,
    runtimes: Vec<Runtime>,
    devices: Vec<Device>,
    pairs: Vec<DevicePair>,
}

impl List {
    /// Refreshes the `simctl list` subcommand's output.
    pub fn refresh(&mut self) -> Result<()> {
        let mut command = self.simctl.command("list");
        command.arg("-j");
        command.stdout(Stdio::piped());
        let output = command.output()?;
        let output: ListOutput = serde_json::from_slice(&output.stdout)?;
        self.device_types = output.device_types;
        self.runtimes = output.runtimes;
        self.devices = output
            .devices
            .into_iter()
            .map(|(runtime, devices)| {
                let simctl = self.simctl.clone();

                devices.into_iter().map(move |device| {
                    Device::new(
                        simctl.clone(),
                        DeviceInfo {
                            runtime_identifier: runtime.clone(),
                            ..device
                        },
                    )
                })
            })
            .flatten()
            .collect();
        self.pairs = output
            .pairs
            .into_iter()
            .map(move |(udid, pair)| DevicePair { udid, ..pair })
            .collect();
        Ok(())
    }

    /// Returns all device types that have been registered with `simctl`.
    pub fn device_types(&self) -> &[DeviceType] {
        &self.device_types
    }

    /// Returns all runtimes that have been registered with `simctl`.
    pub fn runtimes(&self) -> &[Runtime] {
        &self.runtimes
    }

    /// Returns all devices that have been registered with `simctl`.
    pub fn devices(&self) -> &[Device] {
        &self.devices
    }

    /// Returns all device pairs that have been registered with `simctl`.
    pub fn pairs(&self) -> &[DevicePair] {
        &self.pairs
    }
}

#[derive(Debug, Default, Deserialize)]
struct ListOutput {
    #[serde(rename = "devicetypes")]
    device_types: Vec<DeviceType>,
    runtimes: Vec<Runtime>,
    devices: HashMap<String, Vec<DeviceInfo>>,
    pairs: HashMap<String, DevicePair>,
}

impl Simctl {
    /// Returns a list of all device types, runtimes, devices and device pairs
    /// that have been registered with `simctl`.
    pub fn list(&self) -> Result<List> {
        let mut list = List {
            simctl: self.clone(),
            device_types: vec![],
            devices: vec![],
            pairs: vec![],
            runtimes: vec![],
        };
        list.refresh()?;
        Ok(list)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_list() -> Result<()> {
        let simctl = Simctl::new();
        let _ = simctl.list()?;
        Ok(())
    }
}