Chad's Blog

Uninistalling All Gems

Posted by Chad Hendry on 23 Feb 2010

Useful for testing that your environment.rb has everything it needs:

gem list --no-versions | xargs sudo gem uninstall -a

Deploying Rails with Git and Vlad

Posted by Chad Hendry on 22 Feb 2010

Vlad the Deployer’s documentation barely exists. So, I took notes setting up vlad for my personal project, BeerTap.Me, and collected them into this post.

I’m using the following software:

  • Rails 2.3.5
  • Vlad 2.0.0
  • Vlad-Git 2.1.0
  • Ubuntu 9.10 (server)
  • Snow Leopard (dev machine)
  • Apache 2.2
  • Phusion Passenger 2.2.10

Installing Vlad

The vlad gem itself only supports Subversion. There is a separate gem, vlad-git, which contains a plugin for Git. You have to install both gems:

sudo gem install vlad
sudo gem install vlad-git

Next, import vlad’s Rake tasks by adding the following to your Rakefile:

begin
  require 'vlad'
  Vlad.load :scm => :git
rescue LoadError
  # do nothing
end

The server to which you are deploying does not need to have the vlad gem installed, so the begin/rescue allows the exception to be safely swallowed when running Rake on the server. The :scm => :git argument tells vlad to use the git plugin.

Next create a deployment file. By default, vlad looks for this in config/deploy.rb. Mine looks like this:

set :domain, 'beer.chendry.org' # where to ssh
set :deploy_to, '/var/www/BeerTap.Me/' # target directory on server
set :repository, 'git@github.com:chendry/BeerTap.Me' # git repo to clone
set :revision, 'master' # git branch to deploy
set :web_command, 'sudo apache2ctl' # command to start/stop apache

rake vlad:setup

This task needs to be ran only once. It connects to the remote machine and creates the :deploy_to directory and the following directotry structure inside of it:

  • scm/
  • releases/
  • shared/log/
  • shared/pids/
  • shared/system/

You’re probably going to have to tweak the permissions a bit in order to get this to work. I created the deploy directory itself manually, enabled write access for my ssh user, and let vlad create the remaining directories.

rake vlad:update

This is where most of the interesting stuff happens. Not much is documented about what goes on behind the scenes, though, so here’s a brief summary:

First, vlad ssh’s to your remote server’s deploy directory. Then, it blows away everything in scm/repo and replaces it with a fresh clone of the Git repository. Next it tar’s up the branch, creates a release subdirectory named after the current date and time, and extracts the tar into it. Then it deletes the log, public/pids, and tmp/system directories from the freshly deployed Rails site and replaces them with symlinks to shared/log, shared/pids, and shared/system directores, respectively.

Finally, once everything is deployed into the release subdirectory, vlad points the current symlink in the deploy directory to the new release (e.g., /var/www/BeerTap.Me/releases/20100222210521/.) Apache should point to the current directory, as it always points to the current release.

rake vlad:migrate

This task runs rake db:migrate RAILS_ENV=production for the current deployment on the server.

Here is where I ran into my first problem. There is no database.yml in a fresh clone of my rails app. Instead there is a database.yml.example. (I don’t like to keep my production passwords in version control.)

To resolve this, I created a shared/config directory and put my real database.yml there. Then I added a new Rake task to deploy.rb that copies the shared config files into the current release. (Thanks to Helder’s post for the idea:)

namespace :vlad do
  remote_task :copy_config_files, :roles => :app do
    run "cp #{shared_path}/config/* #{current_path}/config/"
  end
end

Now, rake vlaid:migrate runs fine so long as rake vlad:copy_config_files is ran first.

rake vlad:start

NOTE: In vlad 2.0.0, vlad:start has a bug which was reported, months ago, with solution, here and here. Maybe this will be fixed soon, but in the meantime, running the vlad:update and vlad:start tasks as part of the same Rake invocation is the easiest workaround.

The vlad:start task restarts the application by touching tmp/restart.txt and restarts the web server using apache2ctl. I ran into a permission issue here: my ssh user did not have permission to run apache2ctl. So, I changed :web_command to "sudo apache2ctl". However, that too failed with the message:

sudo: no tty present and no askpass program specified
rake aborted!
execution failed with status 1: ssh beer.chendry.org sudo apache2ctl restart

The problem is that there is no terminal and consequently sudo can’t prompt the user for the password. To fix this, I edited the sudoers file (visudo) on the server to allow my ssh user to run apache2ctl without a password:

chendry ALL=(ALL) ALL, (ALL) NOPASSWD: /usr/sbin/apache2ctl

Putting it All Together

Finally, add a rake task to your deploy.rb to do all the steps for a full deploy:

namespace :vlad do
  task :deploy => %w[
    vlad:update
    vlad:copy_config_files
    vlad:migrate
    vlad:start
  ]
end

With that, vlad will do a full deployment to the server: install the latest code, install the configuration files, migrate the database, and (re)start the application/webserver.

GeoKit

Posted by Chad Hendry on 10 Dec 2009

One of the requirements in my current pet Ruby project is to, given the address of a bar, determine its latitude and longitude coordinates.

This is easy with the GeoKit gem:

class Bar < ActiveRecord::Base
  validates_presence_of :name, :address1, :city, :state, :zip, :phone
  acts_as_mappable :auto_geocode => { :field => :street_address }

  private

    def street_address
      parts = [ address1, address2, city, "#{state} #{zip}" ]
      parts.compact.join(', ')
    end
end

Whenever a new bar is saved, GeoKit automatically calls the model object’s street_address method to get a string to pass to a provider. (Providers include Google, Yahoo, Geocoder.us, etc.) GeoKit uses the provider’s response to populate the model’s latitude and longitude attributes:

With this in place, you can query to find all bars within a certain distance from an origin location:

Bar.find(:all, :origin => '68506', :within => 5)

This generates the following SQL:

SELECT
  *,
  (
    ACOS(
      least(
        1,
        COS(0.71180079486811) * COS(-1.68663538782878) *
        COS(RADIANS(bars.lat)) * COS(RADIANS(bars.lng)) +
        COS(0.71180079486811) * SIN(-1.68663538782878) *
        COS(RADIANS(bars.lat)) * SIN(RADIANS(bars.lng)) +
        SIN(0.71180079486811) * SIN(RADIANS(bars.lat))
      )
    ) * 3963.19
  ) AS distance
FROM
  `bars`
WHERE
  (
    (
      (
        bars.lat > 40.7108964735732
        AND bars.lat < 40.8554663264268
        AND bars.lng > -96.7325543757321
        AND bars.lng < -96.5416242242679
      )
    )
    AND
    (
      (
        ACOS(
          least(
            1,
            COS(0.71180079486811) * COS(-1.68663538782878) *
            COS(RADIANS(bars.lat)) * COS(RADIANS(bars.lng)) +
            COS(0.71180079486811) * SIN(-1.68663538782878) *
            COS(RADIANS(bars.lat)) * SIN(RADIANS(bars.lng)) +
            SIN(0.71180079486811) * SIN(RADIANS(bars.lat))
          )
        ) * 3963.19
      ) <= 5
    )
  )

Hello World, Flex

Posted by Chad Hendry on 15 Sep 2009

I created my first Flex application today and it was remarkably painless. Here’s how:

First download and install the Flex 3 SDK. Unzip it anywhere and add the bin subdirectory to your path.

Next create a file called hello.mxml:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
  <mx:Script>
  <![CDATA[
    import mx.controls.Alert;
  ]]>
  </mx:Script>
  <mx:Button id="button" label="Hello, World!"
      click="Alert.show('hi!')" />
</mx:Application>

Then compile it using mxmlc:

Next, embed the compiled .swf file in a webpage using the following HTML:

<html>
  <head>
    <title>Chad's First Flash Application</title>
  </head>
  <body>
    <object type="application/x-shockwave/flash" data="hello.swf"
        width="200" height="200" />
  </body>
</html>

Here’s the result:

iPhone Quickstart

Posted by Chad Hendry on 09 Aug 2009

This tutorial describes how to create an iPhone application using Xcode and Interface Builder. I’m assuming that you have installed Xcode and the iPhone SDK. The application prompts the user for a name and displays a message when the user touches a button.

To begin, start Xcode, and create a new view-based application:

Next, double-click on HelloViewController.xib. This will launch Interface Builder. Open the Library (Tools → Library) if it’s not already visible, and drag a Text Field, Round Rect Button, and Label onto the view window. Arrange them as they appear in the following screenshot. Double-click on the button to edit it’s text.

Next we need to add two instance variables and one method to the HelloViewController class. The two instance variables (outlets) will hold pointers to the UITextField and UILabel controls we added using Interface Builder. The method (action) will be wired up to execute in response to the user touching the Hello button. Go back to Xcode and edit your HelloViewController.h file to look like this:

Next, save everything (important!) and go back to Interface Builder. We need to tell Interface Builder to “wire up” our variables (outlets) and methods (actions) to the controls we placed on the view. To do this, control+click “File’s Owner,” drag to the text field, release, and select the outlet you want to attach. In this case, select “name.”

Do the same thing to attach the UILabel to the message outlet: Control+Click “File’s Owner,” drag to the label, release, and select message.

Now we need to attach the button to the hello: action. This is done similiarly to how we connected our outlets, except in the reverse direction. Control+click the button, drag to “File’s Owner,” release, and select the “hello:” action:

Now that we have our user interface wired up to our view controller, the only thing left to do is implement the hello: action. Open up the HelloViewController.m file in Xcode. You’ll notice a bunch of commented-out code and empty methods. These are all safe to delete. Edit the file so that it looks like this:

At this point, everything is ready to run. To recap, we have:

  1. Created an iPhone View-Based application.
  2. Used Interface Builder to add three controls to our view: a text field, a label, and a button.
  3. Added two outlets to our view controller: one called name for the text field, and another called message for the label.
  4. Added an action called hello: to our view controller.
  5. Used Interface Builder to “wire up” our outlets and actions.
  6. Implemented the hello: action in our view controller.

Click Run → Go. This will compile the application and run it using the iPhone Simulator:

TFS Sucks and I Fixed It

Posted by Chad Hendry on 27 Jun 2009

UPDATE: 2009-09-25: Roughly two months after introducing this console application to our team, the vast majority of the team voted to enable multiple checkouts. Next stop, Git!


Well, not really. But at least now I have a compromise to help wean the company off exclusive checkouts. Here’s the trick: when somebody has a file checked out that you need to modify, do this:

  1. Enable multiple checkouts on the team project.
  2. Remove the lock (but not the checkout) from the workspace that has the file checked out.
  3. Check out the file in your workspace.
  4. Turn multiple checkouts back off.

If the other person beats you to the checkin, he will notice nothing. However, if you check in first, TFS will prompt him to merge before checking in. (Obviously, if your team is accustomed to pessimistic locking, you should let the the other person know what you’re up to.)

I’ve created a program to do this quickly so that others don’t catch on to your mischief. It has a few quirks (no error checking and some assumptions about the working directory) but it works rather nicely:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;

class Program
{
  static VersionControlServer _sourceControl;
  static Workspace _workspace;
  static TeamProject _project;

  static void Main(string[] args)
  {
    ConnectToTfs();

    try
    {
      _project.ExclusiveCheckout = false;

      UnlockFiles(args);
      CheckoutFiles(args);
    }
    finally
    {
      _project.ExclusiveCheckout = true;
    }
  }

  private static void ConnectToTfs()
  {
    var wsInfo = Workstation.Current.GetLocalWorkspaceInfo(".");
    var tfs = TeamFoundationServerFactory.GetServer(wsInfo.ServerUri.ToString());

    _sourceControl = (VersionControlServer)tfs.GetService(typeof(VersionControlServer));
    _workspace = _sourceControl.GetWorkspace(wsInfo);
    _project = _workspace.GetTeamProjectForLocalPath(".");
  }

  static void UnlockFiles(string[] localPaths)
  {
    var serverPaths = GetServerPaths(localPaths);
    var pendingSets = _sourceControl.QueryPendingSets(serverPaths, RecursionType.Full, null, null);

    foreach (var pendingSet in pendingSets)
      UnlockItemsFromPendingSet(serverPaths, pendingSet);
  }

  static string[] GetServerPaths(string[] localPaths)
  {
    return new List<String>(localPaths)
      .Select(arg => _workspace.GetServerItemForLocalItem(arg))
      .Select(arg => arg.ToLower())
      .ToArray();
  }

  static void UnlockItemsFromPendingSet(string[] serverPaths, PendingSet pendingSet)
  {
    foreach (var change in pendingSet.PendingChanges)
    {
      if (serverPaths.Contains(change.ServerItem.ToLower()))
      {
        var ws = _sourceControl.GetWorkspace(pendingSet.Computer, pendingSet.OwnerName);

        if (ws.SetLock(change.ServerItem, LockLevel.None) > 0)
          Console.WriteLine("Lock unset on {0}", change.ServerItem);
      }
    }
  }

  private static void CheckoutFiles(string[] args)
  {
    _workspace.PendEdit(args);
  }
}
Older posts: 1 2 3 4
geico ringtone loop
stop!