Sending Growl messages over a network with Python

6-6-12 – This script is really old and may not work with current versions of Growl. There are some good libraries to choose from and I’d recommend looking at them instead.

Updated 12-4-10 – minor code updates
I have yet another script. This one is actually my first one in Python, which is kind of sad after all the good things I’ve heard about the language. This time I didn’t actually write the entire script – I simply added to one that had already been written to give it more functionality.

The original script was found here: http://the.taoofmac.com/space/Projects/netgrowl and almost all credit goes to it’s author, as I only added a little bit more to it.

What this version does is send a Growl message over UDP to a Mac (running Growl and configured to listen for incoming notifications) using Python. I thought this was really cool – I could script my remote Linux machines (or whatever OS) to send messages to my main Mac.

What I added was the ability to have command line arguments so that it would be easier to change options on the fly and also easier to script.

Running ./netgrowl.py -h will give you the following:

#!/usr/bin/env python

# Updated 12-4-2010
# Altered 1-17-2010 - Tanner Stokes - www.tannr.com
# Added support for command line arguments

# ORIGINAL CREDITS
# """Growl 0.6 Network Protocol Client for Python"""
# __version__ = "0.6.3"
# __author__ = "Rui Carmo (http://the.taoofmac.com)"
# __copyright__ = "(C) 2004 Rui Carmo. Code under BSD License."
# __contributors__ = "Ingmar J Stein (Growl Team), John Morrissey (hashlib patch)"

try:
  import hashlib
  md5_constructor = hashlib.md5
except ImportError:
  import md5
  md5_constructor = md5.new

import struct
# needed for command line arguments
import sys
import getopt

from socket import AF_INET, SOCK_DGRAM, socket

GROWL_UDP_PORT=9887
GROWL_PROTOCOL_VERSION=1
GROWL_TYPE_REGISTRATION=0
GROWL_TYPE_NOTIFICATION=1

def main(argv):

    # default to sending to localhost
    host = "localhost"
    # default title
    title = "Title"
    # default description
    description = "Description"
    # default priority
    priority = 0
    # default stickiness
    sticky = False

    # if no arguments are given, show usage
    if(len(argv) < 1):
        usage()

    try:
        opts, args = getopt.getopt(argv, "hH:t:d:p:s")
    except getopt.GetoptError:
        usage()
    for opt, arg in opts:
        if opt in ("-h"):
            usage()
        elif opt in ("-H"):
            host = arg
        elif opt in ("-t"):
            title = arg
        elif opt in ("-d"):
            description = arg
        elif opt in ("-p"):
            # acceptable values: -2 to 2
            priority = int(arg)
        elif opt in ("-s"):
            sticky = True

    # connect up to Growl server machine
    addr = (host, GROWL_UDP_PORT)

    s = socket(AF_INET,SOCK_DGRAM)
    # register application with remote Growl
    p = GrowlRegistrationPacket()
    p.addNotification()
    # send registration packet
    s.sendto(p.payload(), addr)

    # assemble notification packet
    p = GrowlNotificationPacket(title=title, description=description, priority=priority, sticky=sticky)

    # send notification packet
    s.sendto(p.payload(), addr)
    s.close()

def usage():
    print """Usage: ./netgrowl.py [-hs] [-H hostname] [-t title] [-d description] [-p priority]
Send Growl messages over UDP
  -h help
  -H specify host
  -t title
  -d description
  -p priority [-2 to 2]
  -s make sticky"""
    sys.exit(1)

class GrowlRegistrationPacket:
  """Builds a Growl Network Registration packet.
     Defaults to emulating the command-line growlnotify utility."""

  def __init__(self, application="NetGrowl", password = None ):
    self.notifications = []
    self.defaults = [] # array of indexes into notifications
    self.application = application.encode("utf-8")
    self.password = password
  # end def

  def addNotification(self, notification="Command-Line Growl Notification", enabled=True):
    """Adds a notification type and sets whether it is enabled on the GUI"""
    self.notifications.append(notification)
    if enabled:
      self.defaults.append(len(self.notifications)-1)
  # end def

  def payload(self):
    """Returns the packet payload."""
    self.data = struct.pack( "!BBH",
                             GROWL_PROTOCOL_VERSION,
                             GROWL_TYPE_REGISTRATION,
                             len(self.application) )
    self.data += struct.pack( "BB",
                              len(self.notifications),
                              len(self.defaults) )
    self.data += self.application
    for notification in self.notifications:
      encoded = notification.encode("utf-8")
      self.data += struct.pack("!H", len(encoded))
      self.data += encoded
    for default in self.defaults:
      self.data += struct.pack("B", default)
    self.checksum = md5_constructor()
    self.checksum.update(self.data)
    if self.password:
       self.checksum.update(self.password)
    self.data += self.checksum.digest()
    return self.data
  # end def
# end class

class GrowlNotificationPacket:
  """Builds a Growl Network Notification packet.
     Defaults to emulating the command-line growlnotify utility."""

  def __init__(self, application="NetGrowl",
               notification="Command-Line Growl Notification", title="Title",
               description="Description", priority = 0, sticky = False, password = None ):

    self.application  = application.encode("utf-8")
    self.notification = notification.encode("utf-8")
    self.title        = title.encode("utf-8")
    self.description  = description.encode("utf-8")
    flags = (priority & 0x07) * 2
    if priority < 0:
      flags |= 0x08
    if sticky:
      flags = flags | 0x0100
    self.data = struct.pack( "!BBHHHHH",
                             GROWL_PROTOCOL_VERSION,
                             GROWL_TYPE_NOTIFICATION,
                             flags,
                             len(self.notification),
                             len(self.title),
                             len(self.description),
                             len(self.application) )
    self.data += self.notification
    self.data += self.title
    self.data += self.description
    self.data += self.application
    self.checksum = md5_constructor()
    self.checksum.update(self.data)
    if password:
       self.checksum.update(password)
    self.data += self.checksum.digest()
  # end def

  def payload(self):
    """Returns the packet payload."""
    return self.data
  # end def
# end class

if __name__ == '__main__':

    # send command line arguments to main() function
    main(sys.argv[1:])

iPod touch as an auxiliary display

I played around tonight with making my iPod touch an auxiliary display. I thought it may be neat to just have random real-time public tweets cycle through on it so I made the following. As you can see there’s nothing too smooth about it yet – no AJAXiness implemented as this was purely proof of concept.

Super simple BASH script to monitor a process

Here’s a little BASH script that I made to monitor a virtual machine on an OS X box. Basically when its CPU usage gets higher than 75%, I get a Growl notification and a spoken warning from Bruce – one of the many Mac voices.

One thing to note:

ps -eo %cpu 207 | tail -1

is where the magic is done. Replace ‘207’ with the PID of whatever you want to monitor. It would be wiser to make this script monitor the process by name, but in my case I have the VM running all the time.

#!/bin/sh

# CPU threshold
readonly THRESHOLD=75
alarmState=0

while true; do
    # make the output of our command a variable
    set `ps -eo %cpu 207 | tail -1`
    # turn float into int by truncating from decimal after
    toInt=${1/.*}
    # if our CPU usage is above our threshold
    if [ "$toInt" -gt "$THRESHOLD" ]; then	
        # if we haven't displayed a Growl notification for this alarm
        if [ "$alarmState" -eq "0" ]; then	
            alarmState=1
            # display a Growl notification
            growlnotify -sm "`date` Warning! CPU is at $toInt!"
        fi
        # shout out our warning
        say -v Bruce "Warning! CPU is at $toInt!"
        # consider putting 'sleep' here
    else
        # CPU usage is below our threshold
        alarmState=0
    fi
    # pause the script for a second
    sleep 1
done

Preventing a volume from automatically mounting in OS X

In a previous post I mentioned that I was going to install Snow Leopard on a smaller, separate partition. After doing this, I realized that both partitions were going to be mounted when I booted into either operating system – Leopard or Snow Leopard. I wanted to prevent Spotlight from trying to index the files on both as I’d have duplicate entries for files and applications. I tried to disable indexing the Snow Leopard partition from the Leopard in Spolight’s preferences, but for some reason this configuration was stored universally meaning Snow Leopard would also exclude its own partition and only include Leopard’s. The best thing to do at this point was to keep the partitions from being mounted at the same time, and as separate as possible.

Before we start I must say I don’t recommend anyone who may be slightly scared of the Terminal, “vi”, and or possibly really screwing something up to do this. If you have access to a nerd who knows what they’re doing, I suggest you grab them. I’m not responsible for any hosed systems.

To keep any partition from automatically mounting in OS X do the following:

1. In Terminal, run “sudo vifs”

Why we run this:

The command “sudo” just means run the command after it (in this case “vifs”) as another user, namely root – so that we can make changes to files regular users normally wouldn’t have access to.

The command “vifs” is a utility to safely edit the “/etc/fstab” file – the configuration file we’re going to tell to not mount our partition. The “vi” part is actually from the fact that we’re using the text editor “vi” to change our file.

2. Add the entry of the partition you want to keep from mounting

If the file “fstab” in /etc/ didn’t already exist, vifs will generate it for you. If it did already exist and there were entries, you’ll see them listed. Most users will just see this:

#
# Warning - this file should only be modified with vifs(8)
#
# Failure to do so is unsupported and may be destructive.
#

What we want to do now is add our entry. This can be tricky for people who aren’t familiar with the wonderful world of the vi text editor. Move the cursor down to the last line (with the down arrow key or by pressing shift+G) and then go to the end of that line (by pressing the right arrow or ‘)’). Press ‘i’ and hit the right arrow over one, then press enter to create a new line. At this point you should be able to type text on a new line.

Here’s an example entry of what we’ll put on that line:

UUID=12A4B6C8-1A3B-1C3D-6E8F-123456789876 none hfs rw,noauto

There are four parts to each entry we need to supply: partition, mount point, file system type, and options. All of the things you can do in fstab are way beyond the scope of this article. Running “man fstab” will give you plenty of information if you need to do something different.

In OS X one way to get the UUID for the partition is to go to Disk Utility, right-click on the partition you want to prevent from being automatically mounted, and select “Information”. From there you will be able to copy the “Universal Unique Identifier” line. Pasting it into our Terminal window is as simple as right-clicking and selecting “Paste”.

“none” simply means we’re not giving it a location to mount – this will be handled automatically by OS X.

“hfs” is the type of file system we’re dealing with. Since my partition was a Mac OS Extended (Journaled) type, this is what I used. If the partition is another type, this must match what type it is. This information is also explained in “man fstab” and many places on the web.

“rw,noauto” is our options. “noauto” tells OS X not to automatically mount the partition.

After you’ve added your line, your Terminal should look close to this:

#
# Warning - this file should only be modified with vifs(8)
#
# Failure to do so is unsupported and may be destructive.
#

UUID=12A4B6C8-1A3B-1C3D-6E8F-123456789876 none hfs rw,noauto

To save our file and quit hit “esc”, then type “:wq” and press Enter. If something went wrong and you want to exit vi without making any changes, type “:q!” instead.

Reboot to test this out and you should be good to go. If you want to, check out your system logs in Console to make sure there weren’t any fstab errors.

Impressions of Snow Leopard

So I got Snow Leopard in Friday. Overall, it’s not too bad. Am I going to replace Leopard with it immediately? Unfortunately no. Though I appreciate the many under-the-hood advancements and some great new features, you don’t have to try hard to figure out that Snow Leopard has some issues.

Current issues:

  • No Archive and Install – The first bump in the road I ran into was the lack of “Archive and Install”. Now this isn’t really a bug, but I think it’s worth pointing out. From what I can tell, you now only get two options when installing the latest version of OS X – either to upgrade, or to do a clean install. It appears Apple has removed the “Archive and Install” option that I was looking forward to using. Instead of being able to simply archive my previous installation of Leopard and move big files back over, I instead had to grab these files from a Time Machine backup. This resulted in waiting longer (coming from an external disk) and then dealing with file attribute problems (Time Machine adds certain permissions to the files it generates I’ve learned).
  • Choppy Spaces – I’m a huge fan of Spaces. I have it set up so that when I throw the mouse at the bottom-right corner of my screen, my six spaces fly into view. I can’t live without it anymore – it’s a huge productivity booster. With Leopard I’m used to this being a very smooth process, especially in 9600GT mode. In Snow Leopard this isn’t the case. It’s noticeably choppy, not to mention there is obvious quirkiness you can spot with open windows. Many people wouldn’t really have an issue with this, but it makes me cringe every time I see how slow it is.
  • Jagged / Ugly Fonts – One problem anyone with eyes would notice, though, is font smoothing. People like me who run external monitors have been complaining of jagged fonts even with font smoothing / anti-aliasing on. Before, you could choose between multiple levels of smoothing – now you can only select a single option. The solution for me was to resort to a command in the Terminal, log off, and log back on:
    defaults -currentHost write -globalDomain AppleFontSmoothing -int 2
    

    I didn’t have a problem doing this – my eyes promptly quit bleeding – but most users wouldn’t feel very comfortable throwing some odd looking text into the Terminal.

  • Various compatibility issues – This isn’t necessarily Apple’s fault. Developers have had plenty of time to test their software with Snow Leopard and prepare. The truth is, many applications out there have issues or simply won’t run. We can only wait until these developers push out updated code before these problems are fixed.

Current improvements:

  • Quicktime X – I’m really liking QuickTime X. Incorporating some of the features users could only previously get in QuickTime Pro is great. I absolutely love the screen capture ability.
  • Multiple camera support in Photo Booth – You can now select between multiple cameras in Photo Booth. For the tiny percentage of us users who needed this ability, thank you Apple. I have a Logitech Vision Pro on my main monitor that serves as my main camera when “docked”. It was an ugly process before to get it to work with Photo Booth – open iChat, tell it to use your iSight, simultaneously open Photo Booth, hope that it loads your other camera, close iChat. Now you simply click the camera in a drop down box.
  • Better screen zoom – The screen zoom feature now has a nifty advancement Update: I’ve learned this is a feature in Leopard, they’ve just changed the default behavior – your screen only moves once your mouse reaches the boundaries of your screen. This is wonderful for people like me who use it often to read articles, and occasionally grab the mouse to scroll down. Before, if you moved the mouse a millimeter, the entire screen would shift. Now it only moves if you go all the way to one side.
  • Smarter Finder – Finder can now start its search from the folder you’re browsing. This is great. In Leopard if you were in a network folder and wanted to search for a file you click the little search box, type in your search, and it suddenly starts searching your local files. Why? You’d then have to click back to the remote location and it finally starts searching where you wanted to search in the first place. This is now an option in Finder called “When performing a search, search the current folder”.
  • Icon media preview – You can now preview files simply by mousing over and clicking once. This is awesome for videos or sound files – just hover, click, and watch / listen. It’s a preview before loading Preview. I can’t say this is innovative though – I remember using this in Nautilus in Gnome years ago.
  • Minimize windows to icon – Windows can now minimize to the application’s icon. This provides a much cleaner look when having many windows open, as they simply slide back where they came from. This also works well with the new application expose feature which lets you just look at the windows from that application by clicking. It also makes sense that this feature will be used heavily with a tablet device.

Conclusion:

Of course this is just a portion of what’s out there, good and bad. Am I surprised that there were issues with an operating system on the first day it came out? Of course not. No one should be. In my case I’ve decided to wait until Snow Leopard is ready. There’s no doubt that once it’s updated a few times it will be better than Leopard – this certainly isn’t any sort of XP to Vista sort of deal – it’s just going to take some time. I do plan on installing Snow Leopard on a separate smaller partition and test it as time goes on to keep up with its progress. As soon as it’s ready, I’m there.

Send preset messages automatically with Adium and AppleScript

After coming across this article today on Reddit, I decided to cook up a little AppleScript to accomplish the same task for us Mac users.

Disclaimer: I would never do this to my boss, so if any potential employers are out there scoping out my blog please know that it’s truly for entertainment purposes…

OK so here’s the script.

  • It will send all of the messages in a text file to whatever screen name you choose at defined or random intervals
  • Adium needs to be loaded and you need to be signed in
  • You need to create the text file and know its path. It expects the file to be in plain text and each message to be separated by a new line. I haven’t experimented with anything other than plain text and you could possibly have encoding issues and send some offensive message in Chinese.

Select all of the text below and copy it. Though it’s cut off on the sides it will still grab it all.

-- your Adium IM account name
set imAccnt to "YOURACCOUNTNAMEHERE"

-- screen name of target
set targetName to "TARGETSCREENNAMEHERE"

-- path of input file with messages separated by a newline
-- example: "/Users/JohnDoe/Desktop/messages.txt"
set filePath to "PATHTOFILEHERE"

-- set minimum amount of time to wait before sending messages in seconds
set lowerTime to 200
-- set maximum amount of time to wait before sending messages in seconds
set upperTime to 800

-- open file for input and set variable for text
set textFile to (open for access (POSIX file filePath))
set textInput to (read textFile for (get eof textFile))
close access textFile

-- make a list of messages from every new line in the text file
set messages to every paragraph of textInput

-- make our counter variable the number of how many messages we have
set counter to count messages

-- send messages to our target at different intervals until we're out
repeat with counter from 1 to counter
	-- add a little bit of randomness to the delay
	delay (random number from lowerTime to upperTime)
	-- tell Adium to send the current message to our target
	tell application "Adium"
		-- set our current message to whatever message we're at
		set currentmessage to item counter of messages
		-- start a new chat with the target
		tell account imAccnt to set savedChat to make new chat with contacts {contact targetName} with new chat window
		send savedChat message currentmessage
	end tell
end repeat