Sep 12 2013

We want information… Information…

Even though Apple is known for limiting what you can do on iOS compared to Mac OS X, they actually provide you with some information about the OS and hardware environment you’re running in, assuming you know where to look. Here I’ll be looking at three different places to accumulate this info: the NSProcessInfo, the application bundle, and a Unix system call.

These became useful when we were adding some cheap analytics to a couple of applications we were building. It would be nice to know what versions of the OS our users are running on, for instance, without having to use a third party analytics service.

NSProcessInfo

NSProcessInfo is a Foundation class that lets you examine lots of information about your running context, such as a dictionary of the currently active environment variables, an array of the command-line arguments used to invoke your program, operating system version numbers, the name of your process and its PID, the name of the device, and a whole lot more stuff.

NSProcessInfo has a shared object you can access:

    NSProcessInfo *processInfo = [NSProcessInfo processInfo];

And then you bounce off it to get the data you want:

    NSDictionary *environment = [processInfo environment];
    NSArray *arguments = [processInfo arguments];
    NSUInteger numcore = [processInfo processorCount];

You can find a github repository here, SystemInfo which is an iOS program which loads up a dictionary with bits of data that you can the use for your own purposes.

A brief digression into KVC

There are over a dozen NSProcessInfo calls that are interesting. The straightforward code to pack it all into a dictionary would be pretty tedious:

NSMutableDictionary *info = [NSMutableDictionary dictionary];
info[@"environment"] = [processInfo environment];
info[@"arguments"] = [processInfo arguments];
... repeat eleven more times ...

Foundation includes a technology known as Key-Value Coding, which lets you use strings to query an object. The KVC machinery will look for methods, properties, and instance variables to try to get you the information you need. So, you could do something like this:

info[@"environment"] = [processInfo valueForKey:@"environment"];
info[@"arguments"] = [processInfo valueForKey:@"arguments"];

So you could populate an NSArray with the different keys you wanted and write a loop to call valueForKey:, then stuff it into a dictionary. We can go step further and make the dictionary creation a one-liner by using the method -dictionaryWithValuesForKeys, which does that looping for you.

Here is how NSProcessInfo is queried for its vital information:

NSArray *keys = @[ @"environment", @"arguments", @"hostName",
                   @"processName", @"processIdentifier",
                   @"operatingSystem", @"operatingSystemName",
                   @"operatingSystemVersionString", @"processorCount",
                   @"activeProcessorCount",
                   @"physicalMemory", @"systemUptime" ];

NSMutableDictionary *info = 
    [[processInfo dictionaryWithValuesForKeys: keys] mutableCopy];

The dictionary is made mutable so that we can add information to it through other mechanisms.

The program, when run, will NSLog the info dictionary. I’ll show you results from my iPad 2 running iOS 6.1. These are broken up in the same order as the array of keys:

First up is the general environment the app is running in:

    environment =     {
        "CFFIXED_USER_HOME" = "/private/var/mobile/Applications/28081548-99F4-49C1-B10A-F5B23B84741B";
        "CFLOG_FORCE_STDERR" = YES;
        CLASSIC = 1;
        HOME = "/private/var/mobile/Applications/28081548-99F4-49C1-B10A-F5B23B84741B";
        LOGNAME = mobile;
        NSUnbufferedIO = YES;
        PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
        SHELL = "/bin/sh";
        TMPDIR = "/private/var/mobile/Applications/28081548-99F4-49C1-B10A-F5B23B84741B/tmp";
        USER = mobile;
        "__CF_USER_TEXT_ENCODING" = "0x1F5:0:0";
    };
    arguments =     (
        "/var/mobile/Applications/28081548-99F4-49C1-B10A-F5B23B84741B/SystemInfo.app/SystemInfo"
    );
    hostName = "pheasantpad.local";

The hostName is the Bonjour name of the device, which is based on the name the user gave to the device.

I find it interesting that on iOS, applications are run in an environment that includes a PATH and a SHELL environment variable like their desktop counterparts.

Next comes some process info:

    processName = SystemInfo;
    processIdentifier = 2454;

The processName is the name of your app, and the processIdentifier is the PID of the running Unix process.

Next comes some operating system info:

    operatingSystem = 5;
    operatingSystemName = NSMACHOperatingSystem;

This tells you what OS you’re running on. operatingSystem is an enum, whose values are only of historical interest, such as NSHPUXOperatingSystem and NSOSF1OperatingSystem. There’s very little chance for HP-UX or OSF/1 becoming relevant mobile operating systems running NeXTstep-derived technology. You’ll only see 5 / NSMACHOperatingSystem these days.

You can get the OS version, including the build number. Also the number of processors on the device, and how many are “active.” The documentation isn’t clear on what constitutes “active.”

    operatingSystemVersionString = "Version 6.1.3 (Build 10B329)";
    processorCount = 2;
    activeProcessorCount = 2;

And finally the system memory and the uptime of the system:
?

    physicalMemory = 527417344;
    systemUptime = "282460.9970976667";

The uptime is an NSTimeInterval, measured in seconds. Looks like my iPad was last rebooted about three days ago.

Bundle Info

You can collect your information about the program that you put there, stuff that lives in your Info.plist. You get at your Info.plist stuff through NSBundle, asking the main bundle for its infoDictionary. Some values are pulled out, such as the app name, and the two version strings, and they’re stuffed into info dictionary that so far only has NSProcessInfo information.

NSDictionary *bundleInfo = [[NSBundle mainBundle] infoDictionary];
info[@"name"] = bundleInfo[(__bridge id)kCFBundleNameKey];
info[@"short-app-version"] = bundleInfo[@"CFBundleShortVersionString"];
info[@"app-version"] = bundleInfo[(__bridge id)kCFBundleVersionKey];

Where the future was made yesterday

Depending on how you’re using on the data you’re collecting, you might want to have the build date and time as well. Add it to the info dict as well:

info[@"built"] = @( __DATE__ " " __TIME__ );

The __DATE__ and __TIME__ macros are set automatically by the preprocessor. They’re C literal strings, so this code takes advantage of the preprocessor trick that coalescing “individual” “C” “strings” that are only separated by whitespace, into a single literal string.

As I’m writing this the expression

__DATE__ " " __TIME__

expands to

"Sep 12 2013" " " "10:02:17"

which gets coalesced into

"Sep 12 2013 10:02:17"

and then that value is converted into an NSString:

@( "Sep 12 2013 10:02:17" )

The conversion happens via some of the Objective-C literal syntax.

These are the new values going into the info dictionary:

    name = SystemInfo;
    short-app-version = "1.0";
    app-version = "1.0";
    built = "Sep 12 2013 10:02:17";

Me Tarzan, uname

There’s still some more useful information to be found, this time using a Unix-level API. Don’t forget that OS X and iOS are fundamentally Unix systems, and so have a rich, if cryptic, programming interface available.

uname is a command you can run at the terminal:

% uname
Darwin

It prints the operating system name. (wheee). This can be useful in scripts to decide what kind of OS you’re running on and deciding whether to use particular commands.

The high-powered version of this command takes the -a flag:

% uname -a
Darwin pheasantbook.local 11.4.2 Darwin Kernel Version 11.4.2: \
Thu Aug 23 16:25:48 PDT 2012; root:xnu-1699.32.7~1/RELEASE_X86_64 x86_64

There’s a lot of data there that’s not available from NSProcessInfo. Such as the chip architecture. (x86_64 on my MacBookPro), the Darwin version of the kernel, the kernel build date, and so on.

Don’t worry, you won’t have to NSTask and parse this (which isn’t even possible on iOS). You can use the uname system call from C-land. It’s in third section of the man page, so to read the docs you need to tell man to explicit specify that section:

% man 3 uname

UNAME(3)                 BSD Library Functions Manual                 UNAME(3)

NAME
     uname -- get system identification

LIBRARY
     Standard C Library (libc, -lc)

SYNOPSIS
     #include 

     int
     uname(struct utsname *name);
...

If you do a straight “man uname” you’ll get the docs for the uname command-line utility.

You provide uname-the-system-call with the address of a struct utsname. You would put this struct on the stack and make the call like this:

struct utsname unamen;
int success = uname (&unamen);

You have to include “struct” for the type name. No fancy-pants typedefs here. The structure has five fields: the name of the OS, the network name of the machine, the release and version of the OS, and the specific hardware you’re running on. These are C strings, but easily convertible to NSStrings for inclusion in the info dictionary:

if (success == 0) {
    info[@"uname-sysname"] = @( unamen.sysname );
    info[@"uname-nodename"] = @( unamen.nodename );
    info[@"uname-release"] = @( unamen.release );
    info[@"uname-version"] = @( unamen.version );
    info[@"uname-machine"] = @( unamen.machine );
}

And get results like this:

    uname-sysname = Darwin;
    uname-nodename = PheasantPad;
    uname-release = "13.0.0";
    uname-version = "Darwin Kernel Version 13.0.0: Wed Feb 13 21:37:19 PST 2013; root:xnu-2107.7.55.2.2~1/RELEASE_ARM_S5L8940X";
    uname-machine = "iPad2,3";

iPad2,3, huh? That’s the way Apple identifies their devices. There’s no official list that maps the iPadX,Y hardware identifier to the marketing name of the device (Retina iPad Mini with Soap Dispenser). Luckily one thing the internet is good for is for accumulating lists of stuff, including this one of iDevice models.

Using This Info

OK, so you now have all sorts of information at your fingertips. Should you actually use it? In general, no. In terms of analytics, it can be useful information, especially to see what versions of the OS are actually using your application. But please be sensitive to your user’s privacy, and not collect personally identifiable information (PII), such as the node name or the system name.

Knowing what hardware you’re on could be useful if you’re needing, for example, to reduce the processing load when running on an iPad 1 vs an iPad 4, but you don’t want to base decisions like “is there a camera on this device” on the hardware. Use the provided availability APIs. Similarly, even though you can determine that there are 2 or more processors on your system, you should use NSOperationQueue or Grand Central Dispatch to let the OS make the decisions for load-balancing CPU-heavy work.

Caveats aside, it’s cool being able to peek at some of the grungier implementation details surrounding the application.

2 Comments

  1. Jerry

    The title comes from the BBC TV show “The Prisoner” (also the Iron Maiden song of the same name).

  2. Mark Dalrymple
    Tasha Schroeder

    Hi Jerry, if you’re answering for our giveaway, hop on over to Twitter and reply there! The drawing is in about twenty minutes.

Leave a Comment

Join the discussion. Do not worry, your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>