Better Sleep for Your Mac Media Server

How to improve when your Mac media server goes to sleep after periods of inactivity. In this recipe we combine Power Manager's inactivity trigger with a custom condition to ensure no media is playing.

Do you use a Mac as a media server? Looking for a way to save energy and have your Mac sleep more often? In this recipe, we walk through how to improve when your media server goes to sleep.

We will use iTunes in this recipe but the principle is the same for other media server software such as Sonos and Plex.

We want to put our media serving Mac to sleep whenever:

  • the Mac has been inactive for 30 minutes
  • iTunes is not serving music

The first requirement, 30 minutes of inactivity, is easy to achieve. Power Manager includes an inactivity trigger that fires after a specified period of user inactivity. See How to Make Your Mac Sleep After Inactivity for a recipe to set this up.

It is the second requirement, iTunes not serving, that is tricky. We need a way to determine if iTunes is actively using a media file.

iTunes does support AppleScript and it is possible to ask iTunes through AppleScript if a song is playing. This seems ideal but there is a problem lurking in this approach.

AppleScript Limitations

To be predictable, AppleScript must be executed within the same user session as the running application. Power Manager operates as a computer wide session and not within a single user session; as of Power Manager 4.4 script conditions can not be run within user sessions. So an AppleScript approach in the user session, as a condition, is not easy to arrange.

But what if AppleScript was an option? New questions quickly arise.

What if two users are logged into your Mac and both have iTunes running? Which instance of iTunes should be consulted – one or both? Can we tell within AppleScript who else is running iTunes and if we are the active user?

The complexity of an AppleScript based approach quickly mounts! AppleScript is good but not for every situation.

Open Files

Let’s explore an alternative approach. A more technical approach, but one that can neatly bypass many of the problems with AppleScript.

What if we could see the files iTunes has open? Knowing this we could see if any of the files are media files.

OS X includes lsof. This wonderful command line tool can provide exactly the information we need. lsof is an acronym of sorts for List of Open Files.

Launch iTunes.app and try the following command in Terminal.app to see lsof in action:

lsof -c iTunes

The result is a sizeable list of files that iTunes.app has open.

What about multiple instances of iTunes run by multiple users? Is lsof subject to the same problems as AppleScript? Thankfully not. Multiple users and copies of iTunes running is not a problem for lsof.

When run as super user lsof will return the results of every instance of iTunes. When run as a normal user, only that user’s instance of iTunes will be queried.

Create an Inactivity Triggered Sleep Event

Let’s create the first half of the event:

  1. Launch Power Manager.app

  2. Select Add… to create a new event; or File > New… > New Event…

    Click Add… to begin creating the event

  3. Select the task Power off after inactivity

    Select the Power off after inactivity task

  4. Continue to the next step

  5. Adjust the inactivity period to 0:30:00 to trigger after 30 minutes

    Adjust the inactivity period before sleep

  6. Continue twice passed Time Constraints and Interactive Constraints; nothing to do here

    Continue through the Time Constraints

    Continue through the Interactive Constraints

  7. Continue to Why and name your new event

    Name and describe your new event

  8. Continue and Add to create the basic event

    Confirm the event can be created

The basic event is created and ready. At this stage the event will put your Mac to sleep after 30 minutes of inactivity but not check for iTunes.app’s activity.

The basic event is now ready

Next we will extend the event using Power Manager’s event editor and add a new condition:

  1. Open the event in the event editor; hold down Option and double-click on the event

    Event opened in the event editor

  2. Add a Run Script > Shell Script condition; click Add a condition

    Add a shell script condition

  3. Edit the new condition; click the condition’s Action cog

  4. Copy and paste in the script below into Script

  5. Adjust Short User Name to root; this will check all instances of iTunes

    Paste in the script and set the short user name

  6. Apply and Save to save the new condition

The event is complete. The script below is embedded within your event. This script is run each time inactivity passes 30 minutes increments. The script must pass for the event to perform the sleep.

Open Media File Condition

Below is the script to copy and paste into the event’s script condition:

#!/usr/bin/env perl

use strict;
use warnings;

# Log messages to Console.app's "system.log" and "All Messages"
use Sys::Syslog qw(:standard :macros);

###

my $processNamePrefix = 'iTunes';
my @mediaSuffixes = ( 'aa', 'aac', 'aax', 'aif', 'aiff', 'cdda', 'm3u', 'm3u8', 'm4a', 'mov', 'mp2', 'mp3', 'mp4', 'wav' );

###

openlog("${processNamePrefix}Activity", 'ndelay,pid', LOG_USER);
syslog(LOG_NOTICE, "Checking for $processNamePrefix activity");

# Use the 'List Of Open Files' tool to get a list of open files; limit by process name
my $openFiles = `lsof -c $processNamePrefix`;

# Split output into individual lines
my @openFiles = split(/\n/,$openFiles);

# With each open file...
my @openMediaFiles;
foreach my $openFile (@openFiles) {
	# ...search the line for a media suffix
	foreach my $mediaSuffix (@mediaSuffixes) {
		push(@openMediaFiles,$openFile) if (lc($openFile) =~ /\.\Q$mediaSuffix\E$/)
	}
}

if (scalar(@openMediaFiles) > 0) {
	syslog(LOG_NOTICE, "$processNamePrefix has ".scalar(@openMediaFiles)." media files open");
	syslog(LOG_NOTICE, "$processNamePrefix has open:\n\t".join("\n\t",@openMediaFiles));
} else {
	syslog(LOG_NOTICE, "$processNamePrefix has no open media files");
}

closelog();

exit (scalar(@openMediaFiles) > 0);

The script is written in perl because I am familiar with the language and because perl has great text matching and handling support.

The script has two variables that you might want to change. The name of the process to check against and the types of files to look for. These two variables are $processNamePrefix and @mediaSuffixes.

Media suffixes is a list of lowercase file extensions/suffixes. I suspect you might have media suffixes I have not heard of. So feel free to add them to @mediaSuffixes.

So what does this script actually do?

The script runs lsof asking for a list of all the open files matching the given process name.

With the list of open files, each line is examined and checked against the provided list of media suffixes.

If a file matches one of the media suffixes, a note of that file is made.

When all the files have been checked, the script notes the findings in system.log and returns an appropriate exit code.

The exit code tells Power Manager if the script’s condition has been met. An exit code of 0 tells Power Manager to perform the event’s actions. This is akin to the tradition UNIX process returning 0 to denote success.

If a non-zero value is passed back to Power Manager, the script found open media files and the event’s actions are not performed.

Debugging and Testing

It is important to test any new event. An easy way to do this by adding on-demand behaviour to your new event.

Add on-demand behaviour to aid testing

Add on-demand behaviour by:

  1. Open the event in the event editor; hold down Option and double-click on the event
  2. Edit the event name; click the top Action cog
  3. Select Optional > Behaviours
  4. Enable Can Perform On Demand
  5. Apply and Save the new behaviour

With on-demand behaviour you can manually trigger an event from the status menu bar or through the application. This avoids needing to wait for the inactivity trigger to fire.

Track the condition with Console.app

To further ease debugging and testing, the condition script records its findings to the Mac’s system.log file. Each time the script is run, details of the files found will appear in the log file. Use Applications > Utilities > Console.app to view this file.