Command-line tools via Python and Cocoa

This post is based on a column I wrote for MacTech magazine in 2012. MacTech used to make older columns available online, but they haven’t done that for the past several years for some reason.
I’m planning to go through my older columns and dust off and republish some that I think are still relevant or useful.

Cocoa-Python, also referred to as PyObjC, is a set of Python modules and glue code that allow Python programmers to access many of Apple’s Cocoa frameworks. This allows you to do many things from Python scripting that might otherwise require compiling code in C/Objective-C. To access the Cocoa frameworks, you import them by name, just as you might import a regular Python module.

A quick example: the CoreFoundation framework contains methods to work with user preferences, a bit like the /usr/bin/defaults tool. We can use the CFPreferencesCopyAppValue function in Python simply by importing CoreFoundation, and then calling it like we would a function from a “regular” Python module:

#!/usr/bin/python

import CoreFoundation

print CoreFoundation.CFPreferencesCopyAppValue(
          "HomePage", "com.apple.Safari")

If you run the above code, it will print the current home page you have set in Safari. We’ve successfully used an OS X framework from Python!

A PROBLEM, AND A SOLUTION

I recently had a systems administration problem: I needed some machines to mirror two displays at boot and between login sessions. I found some Objective-C code that handled mirroring, but it did not do exactly what I needed. I decided to try to port it to Python + PyObjC so I could tweak it to my needs. I was able to solve this problem, and in this post I will share the solution.

The Objective-C code I found is part of a display mirroring tool written by Fabian Cañas and available here: http://mirror-displays.googlecode.com/ under the GPLv3 license. The specific code we’ll be referring to is here: https://code.google.com/p/mirror-displays/source/browse/trunk/mirror.m

TRANSLATING COCOA TO PYTHON

Part of the work of porting code from Objective-C to Python requires understanding at least a little bit of C/Objective-C and how it compares to Python. Another part of the work requires understanding how to call Cocoa/Objective-C methods from Python. Let’s look at both parts.
We’ll look at a bit of the Objective-C code:

CGDisplayCount numberOfActiveDspys;
CGDisplayCount numberOfOnlineDspys;
	
CGDisplayCount numberOfTotalDspys = 2; // The number of total displays I'm interested in
	
CGDirectDisplayID activeDspys[] = {0,0};
CGDirectDisplayID onlineDspys[] = {0,0};
CGDirectDisplayID secondaryDspy;
	
CGDisplayErr activeError = CGGetActiveDisplayList(numberOfTotalDspys,
                                                  activeDspys,
                                                  &numberOfActiveDspys);
	
if (activeError!=0) NSLog(@"Error in obtaining active display list: %d\n",activeError);

In C/Objective-C, you declare the type of variables before using them (or when initializing them). Python is not strongly typed, so it doesn’t need type declarations. When translating to Python, we can get rid of any line that only declares a variable and its type, and we can eliminate all type definitions.

After the variable declarations, the code calls an OS X function (CGGetActiveDisplayList). We’ll need to translate that call to Python. That’s a little complicated, so we’ll save the explanation on how to do that for a little later.

After calling CGGetActiveDisplayList, the code checks the return value in activeError; if the value is not 0 it logs an error to the system log. Translating the value check into Python is straightforward.
The (mostly) equivalent Python code is:

number_of_total_dspys = 2

(active_err, active_displays, 
number_of_active_displays) = Quartz.CGGetActiveDisplayList(
                                  number_of_total_dspys, 
                                  None, None)
if active_err:
    print >> sys.stderr, \
        "Error in obtaining active display list: %s" % err
        exit(-1)

Instead of logging the error (which we could do) I’ve opted to just print it to stderr and exit with an error code.

A nice thing about using Python is it’s pretty easy to test things as we go; there’s no lengthy compile cycle. Let’s test this code.

#!/usr/bin/python

import sys
import Quartz

number_of_total_dspys = 2

(active_err, active_displays, 
number_of_active_displays) = Quartz.CGGetActiveDisplayList(
                                  number_of_total_dspys, 
                                  None, None)
if active_err:
    print >> sys.stderr, \
        "Error in obtaining active display list: " % err
        exit(-1)

print "Active displays: %s" % (active_displays, )
print "Number of active displays: %s" 
				% number_of_active_displays)

If you run the code, you should see output similar to this if you have a single display:

Active displays: (69680128,) 
Number of active displays: 1

Or like this if you have two active displays:

Active displays: (69680128, 188848385)
Number of active displays: 2

We seem to get reasonable results from our test (and no errors!), so we can feel a little more confident that we are successfully calling the CGGetActiveDisplayList function using Python. (Since we set number_of_total_dspys to 2, we’ll get back a maximum of two active displays in the list, even if there are more than two.)

Let’s back up a bit and explain how we got from the C/Objective-C snippet to Python. We start with some boilerplate – the standard “shebang” line that tells the OS that this script file should be executed with Python, and we import the sys module so we can output to sys.stderr. But the next import might be confusing: we import a module called “Quartz”. We can access the CGGetActiveDisplayList method from Python once we import it. But you are probably thinking, “The original C/Objective-C code at https://mirror-displays.googlecode.com/svn/trunk/mirror.m imported Foundation/Foundation.h and ApplicationServices/ApplicationServices.h. Shouldn’t we have imported one or both of those frameworks?” You are very perceptive for thinking that.

The Foundation framework gives us access to base Cocoa classes like NSObject, NSString, NSArray, and the like. Right now, we don’t need those, as we’re just going to use Python data types. So we don’t need to import Foundation. But what about ApplicationServices? If you look at Apple’s documentation, that seems to be the correct framework to import if you want access to the Quartz Display Services methods like CGGetActiveDisplayList. But you’ll find that PyObjC doesn’t seem to know about this framework:

% python
Python 2.7.2 (default, Oct 11 2012, 20:14:37) 
[GCC 4.2.1 Compatible Apple Clang 4.0 (tags/Apple/clang-418.0.60)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import ApplicationServices
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named ApplicationServices
>>> 

It turns out that the ApplicationServices framework is a Carbon framework, and not a Cocoa/Objective-C framework, so PyObjC doesn’t (directly) support it.

For reasons I don’t quite understand, PyObjC makes the methods we need available via its Quartz module. (And now you are asking, “How might I have discovered that?” Unfortunately, I can’t remember where I discovered that tidbit. Doing Google searches on “ApplicationServices PyObjC” and poking around does eventually turn up that bit of info, but maybe only if you already know what you are looking for.) In any case, it turns out that to use the Quartz Display Services functions and constants in Python via PyObjC, we need to import the Quartz module.

Fortunately, this special case is a bit of an anomaly. For the most part, to use a Cocoa framework in Python via PyObjC, you can generally just import it by name. The demo Cocoa-Python application we worked in in past months used methods from the Foundation and AppKit frameworks; it imported Foundation and AppKit.

Now that I’ve waved my hands and made one mystery go away, let’s look at the function call itself. In C/Objective-C:

CGDisplayErr activeError = CGGetActiveDisplayList(
                               numberOfTotalDspys,
                               activeDspys,
                               &numberOfActiveDspys);

And in Python:

(active_err, active_displays, 
number_of_active_displays) = Quartz.CGGetActiveDisplayList(
                                 number_of_total_dspys, 
                                 None, 
                                 None)

You might be a little surprised and perplexed by that translation from C to Python! It looks like we’ve moved parameters and return values around willy-nilly. But there is a rhyme and reason. The C/Objective-C method returns a CGDisplayErr in the variable activeError. It takes three parameters. The first specifies the size of the activeDspys array. The second is a pointer (or reference) to the activeDspys array. The third is a pointer to a variable (numberOfActiveDspys) to hold the count of active displays.

Passing pointers to variables so that a function can alter them to provide multiple return values is a code pattern in C that really doesn’t have an equivalent in Python – Python doesn’t really support pointers. Instead, Python allows for a function to return multiple values. So the makers of PyObjC have put in a little magic: whenever you have a method that takes a pointer as a parameter in order to provide an output value, you instead pass None and the return value gets appended to the tuple of return values. (Still confused? See http://pythonhosted.org/pyobjc/core/intro.html and look at the section on Messages and Functions for another explanation of translating Cocoa functions supported by PyObjC to Python.)

In this specific case, we translate the function by passing None for the two by-reference parameters and add those parameters to the list of return values. In this way we get the three values back.
We can use the same ideas to translate the other Quartz Display Services functions from C/Objective-C to Python. Following is a complete Python script to turn on display mirroring. I’ve removed all error checking to make the code shorter and easier to read.

#!/usr/bin/python

import Quartz

number_of_total_dspys = 2

# get active display data
(active_err, active_displays, 
number_of_active_displays) = Quartz.CGGetActiveDisplayList(
                                  number_of_total_dspys, 
                                  None, None)
# get online display data
(online_err, online_displays, 
number_of_online_displays) = Quartz.CGGetOnlineDisplayList(
                                 number_of_total_dspys, 
                                 None, None)
# get id of main display
main_display_id = Quartz.CGMainDisplayID()

if number_of_online_displays == 2:
    # only try to mirror if we have two connected
    # displays
    if online_displays[0] == main_display_id:
        secondary_display_id = online_displays[1]
    else:
        secondary_display_id = online_displays[0]
        
    if number_of_active_displays == 2:
        # mirroring is off, so begin display configuration
        (err, 
         config_ref) = Quartz.CGBeginDisplayConfiguration(
                           None)
        # mirror primary display to secondary display
        err = Quartz.CGConfigureDisplayMirrorOfDisplay(
                  config_ref, secondary_display_id,  
                  main_display_id)
        # apply the changes
        err = Quartz.CGCompleteDisplayConfiguration(
                  config_ref, 
                  Quartz.kCGConfigurePermanently)

Let’s walk through the code.

# get active display data
(active_err, active_displays, 
number_of_active_displays) = Quartz.CGGetActiveDisplayList(
                                  number_of_total_dspys, 
                                  None, None)

We get an array of active display IDs and the number of active displays by calling Quartz.CGGetActiveDisplayList. An active display is one that we can currently draw on. If mirroring is on, the number of active displays will be fewer than the number of online displays because we can’t (directly) draw on the secondary display of a mirror.

# get online display data
(online_err, online_displays, 
number_of_online_displays) = Quartz.CGGetOnlineDisplayList(
                                 number_of_total_dspys, 
                                 None, None)

Here we get an array of online display IDs and the number of online displays. Online displays are essentially the connected displays.

# get id of main display
main_display_id = Quartz.CGMainDisplayID()

Next, we get the ID of the main display: this is the display at (0, 0) in global coordinates and is the one containing the menu bar if mirroring is not enabled.

if number_of_online_displays == 2:
    if online_displays[0] == main_display_id:
        secondary_display_id = online_displays[1]
    else:
        secondary_display_id = online_displays[0]

The next step is to identify the secondary display. We simply compare the first item in the online_displays list with the main_display_id. If they match, the second item in the list is the secondary display; otherwise the secondary display is the first item in the list.

    if number_of_active_displays == 2:
        # mirroring is off, so begin display configuration
        (err, 
         config_ref) = Quartz.CGBeginDisplayConfiguration(
                           None)
        # mirror primary display to secondary display
        err = Quartz.CGConfigureDisplayMirrorOfDisplay(
                  config_ref, secondary_display_id,  
                  main_display_id)
        # apply the changes
        err = Quartz.CGCompleteDisplayConfiguration(
                  config_ref, 
                  Quartz.kCGConfigurePermanently)

Finally, we are ready to turn on mirroring. If we have two active displays, we can mirror them. We must perform this operation in three steps. First, we inform the OS we are starting to change the display configuration with Quartz.CGBeginDisplayConfiguration. In C/Objective-C, calling it looks like this:

CGError err = CGBeginDisplayConfiguration (&configRef);

Notice the pointer to configRef? This allows the function to return an error code and also a configuration reference object (CGDisplayConfigRef configRef). This is another instance where we pass None to the function when calling it from Python and add the configRef parameter to the tuple of return values:

(err,
 config_ref) = Quartz.CGBeginDisplayConfiguration(None)

After telling the OS we’re reconfiguring displays, we call Quartz.CGConfigureDisplayMirrorOfDisplay to tell the OS to mirror the main display to the secondary display. Finally, we commit our changes by calling Quartz.CGCompleteDisplayConfiguration, passing it the constant Quartz.kCGConfigurePermanently, which tells the OS to keep this configuration until told otherwise. Other values are kCGConfigureForAppOnly, which means the change should last only until the current application exits, and kCGConfigureForSession, which means the configuration change should last only until the current user logs out. For our purposes, kCGConfigurePermanently is the right choice.

Try out the code! Make sure you have exactly two displays that are not currently mirrored, and run the script. Your displays should now be mirrored. Run it again. Since the displays are already mirrored, the tool does nothing and silently exits.

PROBLEM SOLVED?

If all we need is to ensure mirroring is on, we have completed our tool. It really should have better error checking and handling, but we could use it as-is. We could name it mirror_displays.py, mark it as executable, and use it to ensure mirroring is enabled.

Remember the original problem I wanted to solve was to enable display mirroring when at the loginwindow. For that, having this script is not enough. I needed also to implement a launchd LaunchAgent that runs in the LoginWindow context. Such a LaunchAgent actually runs the script at the appropriate time and in the appropriate context. Here’s a LaunchAgent plist that would work with the tool we created above:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Disabled</key>
  <false/>
  <key>Label</key>
  <string>com.mactech.displaymirroring</string>
  <key>LimitLoadToSessionType</key>
  <array>
    <string>LoginWindow</string>
  </array>
  <key>ProgramArguments</key>
  <array>
    <string>/Library/Tools/mirror_displays.py</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
</dict>
</plist>

When saved to /Library/LaunchAgents/com.wordpress.managingosx.displaymirroring.plist with the correct owner, group and mode (hint: root:wheel, mode 644), it will run /Library/Tools/mirror_displays.py when the loginwindow displays. The combination of the script and the launchd job will ensure that display mirroring is enabled when the machine is at the loginwindow. Problem solved.

The original command-line mirror tool here https://code.google.com/p/mirror-displays/source/browse/trunk/mirror.m is a little fancier than the basic tool we’ve constructed. Fabian’s code supports toggling mirroring, setting and disabling mirroring, and querying the current mirroring state. It’s certainly possible to replicate all that functionality in Python. If you want a more fully-featured version of the mirroring tool that also does better error handling, I’ve made one available here: https://gist.github.com/gregneagle/5722568

EAT MORE PYTHON – IT TASTES LIKE CHICKEN

Python is a powerful, yet fairly easy-to-use language that is well suited to systems administration tasks. With PyObjC, a Python programmer gains access to a huge set of lower-level Mac OS X facilities. It would not have been possible to create this tool using bash scripting – we needed a scripting language with more capabilities. I hope I’m beginning to convince you to consider using Python, and maybe PyObjC as well, to solve systems administration problems of your own.

Command-line tools via Python and Cocoa

2 thoughts on “Command-line tools via Python and Cocoa

Comments are closed.