Thursday, December 29, 2016

Running Tasks from Swift

For my day job, I write macOS management agent software for LANDESK. A significant part of this job is gathering the current state of Apple Macintosh Computers. Apple provides a system configuration framework that can provide much of the information I need. That framework has "documentation" that can be a challenge to decode. Many times, for quick prototypes, I will call out to the system_profiler command line tool. It gives quick feedback and you can easily see the layout of the information you are dealing with.
Apple's new Swift language has some great features that make calling external programs and digging through their output safe and relatively easy. In this post and in several to come, I will show you how to do this. Disclaimer: I would not consider this "best practices." The code in these posts was written while I was first learning Swift myself and is the result of several quick prototypes. Still, I think it is a decent example of using Swift to run external programs.
Running /usr/sbin/system_profiler will give you a whole lot of information about your Mac. In fact, everything you can get from the "System Information" app shows up in the text output of system_profiler. Since some of this information is gathered every time system_profiler is run, it can take a long time. Fortunately, we can ask system_profiler to just give us information on a specific sub-system by specifying a data type. For instance, we can get just network information or general information about our hardware. Running system_profiler SPHardwareDataType would give us this:
Hardware: 

    Hardware Overview:

      Model Name: iMac
      Model Identifier: iMac15,1
      Processor Name: Intel Core i7
      Processor Speed: 4 GHz
      Number of Processors: 1
      Total Number of Cores: 4
      L2 Cache (per Core): 256 KB
      L3 Cache: 8 MB
      Memory: 32 GB
      Boot ROM Version: IM151.0207.B06
      SMC Version (system): 2.22f16
      Serial Number (system): C02NN4G6FY14
      Hardware UUID: D402F211-62D7-59CA-84D0-F887EFF089DC

You can get a list of data types by running system_profiler -listDataTypes. Running system_profiler from Swift is simple. Swift has a Process class that will take care of the work of starting and managing an external task. The following launchAndGetText function can be copied and pasted directly into an Xcode playground. It will run a shell command found at path with the arguments found in the String array args and return the resulting output in a String. If no text output was generated by the command at path, it returns an empty string.

This function is pretty straightforward. Lines 2 through 4 set up the basic process object. The launchPath property on the process object "ps" is just the path of the program we want to run. I like to pass the full path, not just the program name, primarily for security reasons but also because I don't like to rely on the $PATH environment variable. The arguments property is just an array of strings that will be passed as command line arguments. If you have done any C programming for UNIX, you might be wondering about arg[0] and such. Don't. Just put the arguments to the program, not the name of the program itself into the arguments array. You can look at the example later in the post if this isn't making sense.
Lines 6 and 7 attach a Pipe to the Process object's standard out stream. This Pipe lets you read the bytes coming out of the running processes standard out stream. So anything the process normally prints to the terminal is available for your program to read. You can also hook up pipes to standard error to get error messages and standard in if you want to send data to the process. Lines 9 and 10 launch the process and wait for it to finish. There are ways to get the termination status from the process. We will take a look at that another time.
The gist that follows shows an example of using launchAndGetText to get the hardware UUID from a Mac.

You can see on lines 1 and 2 that we are calling system_profiler with a single command line argument, SPHardwareDataType. This will return the text output from system_profiler as a String. We can use the Swift String's component method to break the text into lines of text in an array of Strings. Finding the UUID is then a simple matter of walking through the array and looking for the string "Hardware UUID". Swift provides several ways of digging through arrays and I will spend several posts talking about them in the future. For now I just use the for in loop that is similar to what you would find in most programming languages.
This approach of grabbing the plain text output of system_profiler works great for simple information like the text we get using the SPHardwareDataType argument. Not all everything we might want to get out of system_profiler is that simple. For example, the output of SPNetworkDataType is hierarchical with a level of interfaces, each interface having various bits of information like interface names, addresses, proxy information, and so on. Here is an example of what you get for two interfaces:

    Wi-Fi:

      Type: AirPort
      Hardware: AirPort
      BSD Device Name: en0
      IPv4 Addresses: 192.168.0.4
      IPv4:
          AdditionalRoutes:
              DestinationAddress: 192.168.0.4
              SubnetMask: 255.255.255.255
          Addresses: 192.168.0.4
          ARPResolvedHardwareAddress: 58:8b:f3:ae:9d:c9
          ARPResolvedIPAddress: 192.168.0.1
          Configuration Method: DHCP
          ConfirmedInterfaceName: en0
          Interface Name: en0
          Network Signature: IPv4.Router=192.168.0.1;IPv4.RouterHardwareAddress=58:8b:f3:ae:9d:c9
          Router: 192.168.0.1
          Subnet Masks: 255.255.255.0
      IPv6:
          Configuration Method: Automatic
      DNS:
          Domain Name: Home
          Server Addresses: 192.168.0.1, 205.171.3.25
      DHCP Server Responses:
          Domain Name: Home
          Domain Name Servers: 192.168.0.1,205.171.3.25
          Lease Duration (seconds): 0
          DHCP Message Type: 0x05
          Routers: 192.168.0.1
          Server Identifier: 192.168.0.1
          Subnet Mask: 255.255.255.0
      Ethernet:
          MAC Address: 20:c9:d0:44:0e:a1
          Media Options: 
          Media Subtype: Auto Select
      Service Order: 1

This is a small subset of the network information that comes out of system_profiler. You get all this and more for each interface on your computer. Finding the MAC address for a specific interface becomes complicated using this unstructured output. Fortunately, system_profiler will also output information in Apple's plist format and Swift knows exactly what to do with that. In my next post, we will take a look at taking XML from system_profiler and putting it into Swift data structures that can be easily navigated.

No comments:

Post a Comment