OSX Uninstallers the Easy Way

October 22, 2008

Back in the old days at LimeWire one of the many tasks I took on was building the installers for all platforms, and I’ve carried on my installer hacking with LittleShoot. While installers are anything but glamorous, I’m oddly obsessed with them I think because they’re your users’ first introduction to your program. They need to be simple, and they need to work.

One of Mac’s quirks has always been a lack of uninstallers, leading to widespread global frustration, decreased productivity, and some say a leading cause of the recent financial crisis (a small minority).

Salvation is at hand.  LittleShoot’s uninstaller is ridiculously simple. It’s a little snippet of AppleScript that just runs a bash file loaded into it’s application bundle using “do shell script” to run an external script file. Within this framework, your entire uninstaller is basically a bash script. Here are the steps to get this working:

  1. Copy the following AppleScript into a file called ‘uninstaller.scpt’ and open it in Script Editor. You can also save the binary script from our SVN here and open it in Script Editor directly (easier).
  2. on onConfirmUninstall()
    set applicationName to “LittleShoot”
    display dialog “Are you sure you want to uninstall ” & applicationName & “?”
    set uninstallScript to quoted form of POSIX path of (path to resource “uninstall.bash”)
    do shell script “bash ” & uninstallScript with administrator privileges
    display dialog “Successfully Uninstalled ” & applicationName buttons {“OK”} default button “OK”

    on error err
    if err contains “User canceled” then
    display dialog “Canceled ” & applicationName & ” Uninstall” buttons {“OK”} default button “OK”
    display dialog “We’re sorry, but there was an error uninstalling ” & applicationName & ” described as: ” & err buttons {“OK”} default button “OK”
    end if
    end try
    end onConfirmUninstall


  3. Change the line ‘set applicationName to “LittleShoot”‘ to ‘set applicationName to “[your application name]”‘
  4. Choose File->Save As… in AppleScript Editor and save this script as an application bundle that’s run only with no startup screen (the options at the bottom when you choose Save As…). 
  5. Navigate via the Terminal to where you saved the bundle and cd into the “Contents/Resources” directory. In our case that’s “LittleShootUninstaller.app/Contents/Resources,” so it’s Contents/Resources within the app bundle.
  6. Create an uninstall.bash file in Contents/Resources. As you can see in the AppleScript above, that’s the file the AppleScript looks for and executes.
  7. Put whatever you need in uninstall.bash to uninstall your application. The script will run with administrator privileges, so you can really do whatever you want here. Here’s the LittleShoot uninstall script to get you going, although this is a little quirky because we use things like launchd that most applications don’t use. You can also grab this directly from our SVN here.
  8. #!/usr/bin/env bash 

    function die()
      echo $*
      exit 1

    function cleanAndDie()
      die “LittleShoot is already uninstalled”

    function clean()
      local plist=~/Library/LaunchAgents/org.lastbamboo.littleshoot.plist
      test -f $plist && launchctl unload $plist
      rm -rf ~/Applications/LittleShoot.app
      rm -f ~/Library/LaunchAgents/org.lastbamboo.littleshoot.plist
      rm -rf /Library/Receipts/littleshoot.pkg
      rm -rf ~/Library/Receipts/littleshoot.pkg
      rm -rf ~/.littleshoot
      rm -rf ~/Applications/LittleShootUninstaller.app

    function remove()
      rm -rf $1 || die “Could not remove file: $1”

    # If it looks like we’ve already uninstalled, just make sure to remove everything again and die.
    test -e ~/Applications/LittleShoot.app || cleanAndDie

    launchctl stop org.lastbamboo.littleshoot || die “Could not stop LittleShoot”
    launchctl unload ~/Library/LaunchAgents/org.lastbamboo.littleshoot.plist || die “Could not unload”
    rm -rf ~/Applications/LittleShoot.app || die “Could not remove LittleShoot”
    rm -f ~/Library/LaunchAgents/org.lastbamboo.littleshoot.plist || die “Could not remove plist”

    # We go through all this because the package file is placed differently on Tiger, Leopard, etc.
    test -e $globalReceipt && remove $globalReceipt
    test -e $localReceipt && remove $localReceipt
    rm -rf ~/.littleshoot || die “Could not remove LittleShoot config folder”
    rm -rf ~/Applications/LittleShootUninstaller.app || die “Could not remove LittleShoot uninstaller”


So that’s about all she wrote. You basically just have to set up the uninstaller AppleScript application bundle, and then you can just edit your bash script from then on. The application bundle will use whatever script is stored in its Contents/Resources/uninstall.bash file. I’ve played around with a lot of different options for doing this, and the straight script file approach kills the other options in terms of maintainability and flexibility. Note that if the uninstaller encounters an error, the AppleScript will display a dialog to the user with anything your script has echoed, so make those errors informative. You also of course have to include the uninstaller application bundle in your installer.

In the end, your users have a simple uninstaller they can just double click on, and you have a really easy way to write and maintain your uninstaller code.

Back to allowing you to post LittleShoot files to Twitter…


Amazon Web Services vs. Google App Engine: The Race to the One-Click Cloud

August 27, 2008
One-Click Shopping

Can Amazon Build the One-Click Cloud?

It’s a great time to program for the cloud, no matter what Ted Dziuba’s entertaining but barely coherent rants have to say (will someone get that guy some experience?). Amazon and Google are going toe-to-toe, with Amazon’s addition of sorting in Simple DB bringing it up to par with Google App Engine’s Datastore API. Sorting was the biggest missing piece in Simple DB and the most compelling reason to choose the Datastore API instead. No longer.  

But Google App Engine (GAE) and the Datastore API still win. Here’s why:

  1. The Datastore API is projected to be 10x cheaper. $0.15-$0.18 per GB-month sounds a lot better than Simple DB’s $1.50 per GB-month.
  2. GQL. GAE’s SQL subset is just brain dead simple. As adept as programmers are at learning new frameworks, it’s nice to have something brain dead every once in awhile. Simple DB takes a few more cycles to learn (brain cycles that is — more coffee and such. Modafinil perhaps? Anyone tried it? I’m curious).
  3. GAE has better Object Relational Mapping (ORM). GAE basically uses Django’s sweet ORM system. You’ve got to jump through a lot more hoops to get something as nice with Simple DB. 
  4. GAE automatically scales the web application, not just the database. With Amazon, you have to add load balancing and bring machines up and down yourself, even if you’re using Simple DB. While there are third-party tools to help, they’re not built-in. Again, GAE is brain dead here.  

Sure, App Engine only supports Python. The ultimate question, though, is what functionality can you get in the end? For web apps, App Engine gives you more, particularly for scaling (which is kind of the whole point). Don’t know Python? Learn it. It will save you time in the end. Instead of endlessly fiddling with your load balancer and custom scripts for bringing instances up and down, you’ll spend your time adding the next killer feature your users will love.

In the end, the Amazon/Google “main event” is a huge win for you, me, and our users. The sorting announcement from Amazon comes on the heals of a flurry of other new features from both companies, including Amazon’s impressive persistent storage addition for EC2 called the Elastic Block Store, querying by attributes on Simple DB, GAE’s support for 10 applications per user instead of 3, GAE’s batch writes, etc. Neither one is pulling any punches, and the tools at our disposal as developers are progressing at a breathtaking pace as a result.

Amazon’s is clearly the more complete offering (you can do anything on it, in any language), but it needs to learn from Google’s focus on the dominant deployment scenarios.  Amazon could easily win if it does the following:

  1. Makes Simple DB pricing competitive with Google’s projected prices.
  2. Adds a query language for Simple DB along the lines of GQL.
  3. Adds automatic scaling for web applications, not just the database.
  4. Offers complete deployment solutions for the dominant web applications frameworks, from Tomcat/Spring/Hibernate to Django and Zend, with ORM models already adapted to Simple DB, instances automatically replicated with traffic, etc. Basically the same thing as App Engine for more web app frameworks than App Engine supports and adapted to the Amazon platform. Sure, there are third-party solutions for some of this stuff, but those will never be trusted as much as something offered directly from Amazon.

I’m a big fan of Amazon and Werner Vogels (one of the most innovative people in the industry, and also apparently a pretty nice guy), but Amazon desperately needs to learn from what Google has done. It’s ultimately a question of “usability” for developers. The originators of “one-click shopping” are losing in the game they practically invented. 

Amazon needs to turn on the one-click cloud.

P2P in Flash 10 Beta — the Questions Facing a YouTube, Skype, and BitTorrent Killer

May 21, 2008

As I’ve reported, the inclusion of P2P in Flash 10 Beta represents a fundamental disruption of the Internet platform. As with all disruptions, however, this one will progress in fits and starts. Flash 10’s details limit the full power of its P2P features. While features like VoIP will be fully enabled, it will take some ingenuity to turn Flash 10 into a more generalized P2P platform. Here are the issues:

1) Flash Media Server (FMS)

You’ll need Flash Media Server (FMS) to take advantage of Flash P2P. At $995 for the “Streaming Server” and $4,500 for the “Interactive Server”, FMS is beyond the reach of most developers working on their own projects, severely limiting Flash P2P’s disruptive potential. In an ideal world, the new P2P protocols would be openly specified, allowing open source developers to write their own implementations. As it stands now, a single company controls a potentially vital part of the Internet infrastructure, and encryption will likely thwart the initial reverse engineering efforts of open source groups like Red5.

2) No Flash Player in the Background

As David Barrett (formerly of Akamai/Red Swoosh) has emphasized on the Pho List, Flash Player only runs when it’s loaded in your browser. As soon as you navigate to another page, Flash can no longer act as a P2P server. P2P programs like Red Swoosh, BitTorrent, and LittleShoot don’t have this limitation, and it means Flash can’t save web sites as much bandwidth as those full-blown applications can. This limits but does not eliminate Flash’s threat to CDNs. Sure, you could get around this using AIR, but that creates another major barrier to adoption.

3) Usability

While Flash 10 has the ability to save files to your computer and to load them from your computer (essential for P2P), it pops up a dialog box each time that happens. While this is an important security measure, it cripples Flash 10’s ability to mimic BitTorrent because you’d have dialogs popping up all the time to make sure you as a user had authorized any uploads of any part of a file.

4) Limited APIs

While all the required technology is there in the Real Time Media Flow Protocol (RTMFP), ActionScript’s API limits some of the P2P potential of Flash 10. P2P downloading breaks up files into smaller chunks so you can get them from multiple other computers. Flash 10 can only save complete files to your computer — you can’t save in small chunks. As a result, you’d have to use ActionScript very creatively to achieve BitTorrent or LittleShoot-like distribution or to significantly lower bandwidth bills for sites serving videos. It might be possible, but you’d have to work some magic.

So, that’s the deal. There’s still a lot more documentation coming our way from Adobe, so there are undoubtedly useful nuggets yet to be discovered.

Even given all these limitations, however, the key point to remember is the Internet has a new, immensely powerful protocol in its arsenal: Matthew Kaufman and Michael Thornburgh’s Real Time Media Flow Protocol (RTMFP). While Flash might use it primarily for direct streaming between two computers now (think VoIP), it introduces the potential for so much more.

Keep your helmet on.

P2P in Flash 10 Beta – a YouTube, Skype, and BitTorrent Killer

May 16, 2008

The inclusion of p2p in the Flash 10 beta threatens to bring down everyone from YouTube to Skype. Using P2P, Flash sites will be able to serve higher quality video than YouTube at a fraction of the cost. Meanwhile, the combination of the Speex audio codec and the Real Time Media Flow Protocol (RTMFP) will enable sites to seamlessly integrate VoIP without requiring a Skype install. The impact of this change is hard to fathom. We’re talking about a fundamental shift in what is possible on the Internet, with Flash demolishing almost all barriers to integrating P2P on any site.

Hank Williams and Om Malik have discussed the potential for Flash 10 to be used for P2P CDNs, and they’re largely right on. The biggest problem I see with P2P CDNs is oddly latency, however. While P2P theoretically enables you to choose copies of content closer to you on the network, you still have to negotiate with a server somewhere to establish the connection (for traversing NATs), nullifying the P2P advantage unless you’re talking about really big files. As Hank identifies, the sites serving large files are the CDN’s best customers, so we are talking about a significant chunk of the CDN business up for grabs. That said, CDNs could easily start running Flash Media Servers themselves with integrated RTMFP. They’ve already addressed the server locality problem, and taking advantage of Flash deployments would simply be an optimization. Whether the CDNs will realize this shift has taken place before it’s too late is another question.

To me, the really vulnerable players are the video sites themselves and anyone in the client-side VoIP space. Writing a VoIP app is now equivalent to writing your own Flash video player. All the hard stuff is already done. Same with serving videos. You no longer have to worry about setting up an infinitely scalable server cluster — you just offload everything to Flash. No more heavy lifting and no more huge bandwidth bills. In the BitTorrent case, it’s mostly a matter of usability. As with Skype, you no longer need a separate install. Depending on what’s built in to the Flash Media Server, you also no longer need to worry about complicated changes on the server side, and downloads will happen right in the browser.

The stunning engineering behind all of this should be adequately noted. The Real Time Media Flow Protocol (RTMFP) underlies all of these changes. On closer inspection, RTMFP appears to be the latest iteration of Matthew Kaufman and Michael Thornburgh’s Secure Media Flow Protocol (SMP) from Adobe’s 2006 acquisition of Amicima. Adobe appears to have acquired Amicima specifically to integrate SMP into Flash, now in the improved form of RTMFP. This is a very fast media transfer protocol built on UDP with IPSec-like security and congestion control built in. The strength of the protocol was clear to me when Matthew first posted his “preannouncement” on the p2p hackers list. Very shrewd move on Adobe’s part.

Are there any downsides? Well, RTMFP, is for now a closed if breathtakingly cool protocol, and it’s tied to Flash Media Server. That means Adobe holds all the cards, and this isn’t quite the open media platform to end all platforms. If they open up the protocol and open source implementations start emerging, however, the game’s over.

Not that I have much sympathy, but this will also shift a huge amount of traffic to ISPs, as ISPs effectively take the place of CDNs without getting paid for it. While Flash could implement the emerging P4P standards to limit the bleeding at the ISPs and to further improve performance, this will otherwise eventually result in higher bandwidth bills for consumers over the long term. No matter — I’d rather have us all pay a little more in exchange for dramatically increasing the numbers of people who can set up high bandwidth sites on the Internet. The free speech implications are too good to pass up.

Just to clear up some earlier confusion, Flash Beta 10 is not based on SIP or P2P-SIP in any way. Adobe’s SIP work has so far only seen the light of day in Adobe Pacifica, but not in the Flash Player.

Decentralized Twitter a Bad Idea

May 5, 2008

Michael Arrington’s post on creating a decentralized Twitter is theoretically interesting but practically naive.  Martin Fowler’s First Law of Distributed Computing continually rings true: don’t distribute your objects.  Why?  Because it’s hard.  In every case I’ve ever seen, it’s orders of magnitude harder to distribute a task than it is to centralize it.

Search is the quintessential example.  When I wrote the search algorithms and protocols for the second-generation Gnutella network at LimeWire, the degree of coordination between millions of network nodes was staggering.  It worked amazingly well all things considered, but it still could never compete with the speed or collaborative filtering of centralized search.  Most importantly, it could never compete with simplicity of centralization.  What took us 6 months to distribute would have taken a couple of days to centralize.  Distributed networks also make updating much harder, as you’re now forced to update every instance of your software running on any machine in the world.  It’s a pain in the a$$.  If you have a choice, centralized search wins every time.  That’s one of the reasons LittleShoot follows Fowler’s law whenever possible.  Something about working better and taking less time makes it appealing.

Distributed computing has shown itself to be particularly useful for moving around large files.  In Twitter’s case, you’re working from the opposite extreme: processing a high volume of tiny messages.  This screams centralization.

Centralization is not the reason Twitter can’t scale.  They can’t scale because, well, they just haven’t written an architecture that scales.  Granted the mobile space is still largely DIY, so they have to roll much of their own code.  That’s really a pretty lame excuse, though, especially given their resources and the time they’ve had to figure it out.  My buddies over at Mobile Commons face similar issues processing huge volumes of mobile messages, and they don’t have these issues.  I’m convinced Twitter would be flying if Ben Stein and Chris Muscarella at Mobile Commons were in charge, and I honestly think it’s because they’re just better programmers.

I’m a big Evan Williams fan and was thrilled to meet him for the first time down at SXSW, but that doesn’t mean I have faith in his or his team’s ability to learn how to scale a complex architecture overnight.  A distributed architecture would make their task orders of magnitude harder.  Don’t do it fellas.

Atlassian JIRA — Automating the Standalone Install on MySQL

February 2, 2008

I’ve started thinking of my coding wanderings as akin to Alice’s rabbit holes — magical new places I play around in for probably a little too long. Automating sysadmin-type work with shell scripts has become my latest rabbit hole. Quickly running new services on Amazon’s EC2 is my inspiration.

So, this is the first little snippet, a simple initial building block that will become a part of larger scripts down the road. For those who don’t know, Atlassian has started giving away free licenses to all of their products to open source projects, so this gives you access to JIRA, Bamboo, Confluence, FishEye, Clover, Crowd, etc. These tools are amazingly useful and are all the best or amongst the best at what they do. Check out the Atlassian web site for more info.

This script automates the two trickiest parts of installing JIRA:

  1. Connecting to your database. In this case we connect to MySQL.
  2. Customizing the port to run JIRA on.

In the first instance, the script automatically downloads the MySQL JDBC driver, creates the JIRA database, and configures the JIRA user name and password for MySQL. The port customization is something you frequently want because so much runs on 8080 by default. These tasks are more annoying than tricky, but this script makes them a breeze.


  1. MySQL already running on the default port.
  2. You need to know your MySQL root password. The script will use it to create the JIRA database and to set permissions for the JIRA MySQL user.
  3. A downloaded version of JIRA standalone from the Atlassian web site. This will be a file called atlassian-jira-VERSION-standalone.tar.gz. The script just looks for a file starting with “atlassian-jira” and ending in “tar.gz” in the current directory.
  4. Java installed with JAVA_HOME set.

Future scripts will also include automated installing and configuring of MySQL as well as Java, but for now you need them configured ahead of time. I chose to run JIRA standalone because in my experience getting the separate wars to play nicely with my existing wars was tricky. In particular, some of the Atlassian war files take awhile to start up and don’t shut down as cleanly as they should. Using the standalone versions insures they won’t interfere with your other webapps.

When you have JIRA downloaded, MySQL running, and Java configured, go ahead and download the script from the LittleShoot web site.

Here’s all you need to run:


The script will guide you through the process of configuring and running JIRA, and it should be really self-explanatory. When the script is done, you’ll still need to run through JIRA’s configuration procedure within the browser, but the script has taken care of the hard part.

If you need to install JIRA from another script, you can also run something like the following, modifying it for your values of course.

./jira.bash jirauser jirapwd yourMySql_root_password adamfisk

The last argument is the user name of the user on the system who should own the jira directory.

Below is the full script.

#!/usr/bin/env bash
# This script performs all the JIRA configuration and setup for running
# JIRA on MYSQL.  This includes creating the JIRA database and creating
# a user on the database.
# If no arguments are passed to the script, it prompts you for the
# data it needs.  Otherwise, you must pass all the required data on the
# command line.  This makes it easier to incorporate this script into
# other scripts if desired.
# If you decide to pass in arguments, they are (in order):
# 1) The name of the new jira user in the database.
# 2) The password of the new jira user in the database.
# 3) Your MYSQL root password to create the JIRA database.
# 4) The user account to install JIRA under.  This account should
#    already exist on the system.
# To run this script:
# That file should be the downloaded copy of JIRA standalone.
# If you have any problems, please see the excellent guide at:
# http://confluence.atlassian.com/display/JIRA/Setting+up+JIRA+Standalone+and+MySQL+on+Linux

function die
echo $1
exit 1

ls ./atlassian-jira-*.tar.gz > /dev/null || die "The Atlassian JIRA tar.gz file must be in the current directory.  Have you successfully downloaded JIRA standalone?"

netstat -na | grep 3306 > /dev/null || die "MySQL does not appear to be running on port 3306.  JIRA cannot be installed without MySQL running"

function askUser
echo "Please enter your JIRA database user name:"

echo "Please enter your JIRA database password:"

echo "Please enter your MySQL root password:"

echo "What's the name of the user account on this machine you'd like to install JIRA under?"

if [ $# -ne "$ARGS" ]
    if [ $# -ne "0" ]
        echo "Usage: jira.bash jira_mysql_user_name jira_mysql_password mysql_root_password user_account"
        echo "You can also just run ./jira.bash to have the script guide you through the setup process."

echo "............................................................"
echo "  Hello $USER, let's start setting up JIRA standalone."
echo "............................................................"

function modifyPort
  echo "What port would you like to use for JIRA?  The default is 8080."
  echo "What shutdown port would you like to use for JIRA?  The default is 8005."
  echo "OK, got it.  Proceeding with install."

echo "Would you like to change the port JIRA runs on from the default of 8080? [y/n]"
case $CHANGE_PORT in
  modifyPort || die "Could not modify port"
  modifyPort || die "Could not modify port"
  echo "OK, using default port of 8080.  Proceeding with install."

function installJira
echo "Expanding `ls ./atlassian-jira-*.tar.gz`..."
tar xzf `ls ./atlassian-jira-*.tar.gz` || die "Could not open jira tgz file.  Aborting."

# Add a symbolic link to whichever version of JIRA we're running.
ln -s `ls | grep atlassian-jira-` jira

echo "Downloading MYSQL JDBC connector..."

# Somewhat bad to hard code this, but I don't think JIRA users alone will have much of an impact on this server.
curl -o mysqlj.tgz http://mirrors.24-7-solutions.net/pub/mysql/Downloads/Connector-J/mysql-connector-java-5.1.5.tar.gz
tar xzf mysqlj.tgz
mv mysql-connector-java-5.1.5/mysql-connector-java-5.1.5-bin.jar jira/common/lib || die "Could not move myql jdbc jar"

echo "Customizing server.xml..."
cp jira/conf/server.xml jira/server.xml.copy
perl -pi -e s/Server\ port=\"8005\"/Server\ port=\"$CUSTOM_SHUTDOWN_PORT\"/g jira/conf/server.xml || die "Could not set shutdown port"
perl -pi -e s/Connector\ port=\"8080\"/Connector\ port=\"$CUSTOM_PORT\"/g jira/conf/server.xml || die "Could not set JIRA port"
perl -pi -e s/username=\"sa\"/username=\"$JIRA_USER_NAME\"/g jira/conf/server.xml || die "Could not modify jira user name"
perl -pi -e s/password=\"\"/password=\"$JIRA_PWD\"/g jira/conf/server.xml || die "Could not modify jira password"
perl -pi -e s/driverClassName=\"org.hsqldb.jdbcDriver/driverClassName=\"com.mysql.jdbc.Driver/g jira/conf/server.xml
perl -pi -e s/jdbc:hsqldb:\\$\{catalina.home\}\\/database\\/jiradb\"/jdbc:mysql:\\/\\/localhost\\/jiradb?autoReconnect\=true\&\;useUnicode\=true\&\;characterEncoding\=UTF8\"\\/\>/g jira/conf/server.xml || die "Could not set jdbc"
perl -pi -e s/minEvictableIdleTimeMillis\=/\/\"20\"\ \\/\>--\>/g jira/conf/server.xml || die "Could not finish comment"

echo "Customizing entityengine.xml..."
cp jira/atlassian-jira/WEB-INF/classes/entityengine.xml jira/entityengine.xml.copy || die "Could not make entityengine backup"
cp jira/atlassian-jira/WEB-INF/classes/entityengine.xml . || die "Could not copy entityengine to current directory"

perl -pi -e s/name=\"defaultDS\"\ field-type-name=\"hsql\"/name=\"defaultDS\"\ field-type-name=\"mysql\"/g entityengine.xml || die "Could not set entityengine database to MYSQL"
perl -pi -e s/schema-name=\"PUBLIC\"//g entityengine.xml || die "Could not remove public schema from entiry engine"

mv entityengine.xml jira/atlassian-jira/WEB-INF/classes/ || die "Could not move entity engine"

chown -R $USER_ACCOUNT jira || die "Could not set permissions to specified user: $USER_ACCOUNT"

cat < jira.sql
create database if not exists jiradb character set utf8;
flush privileges;
mysql -uroot -p$MYSQL_ROOT_PWD < jira.sql || die "Could not set up database for JIRA.  Is your root password correct?"
echo "Starting JIRA on port $CUSTOM_PORT..."
./jira/bin/startup.sh || die "Could not start JIRA"

echo ""
echo "-----------------------------------------------------------------------------------------------------------------"
echo "  Great, JIRA's starting up.  You should be able to access it momentarily on port $CUSTOM_PORT on this machine."
echo "-----------------------------------------------------------------------------------------------------------------"


exit 0