2010-11-11

Open HTML::Mason stack trace files in Emacs by clicking links

Introduction

You're debugging your web application and are suddenly faced with all so familiar error page, presenting you with an error message and, naturally, a stack trace. You wade through the call stack, finding respective files, entering line numbers - much more hassle than it needs to be.

Wouldn't it be nicer to have stack frame references as links which you could click to open in editor, positioned on the right line, looking like this:

HTML::Mason stack trace with links

Here's how to do it with very little JavaScript, Perl and configuration work. This example deals with HTML::Mason stack traces and the Emacs editor. However, the method can be adapted to other frameworks or editors without much diffuculty.

Step 1. Install Greasemonkey

Greasemonkey is a Firefox extension for transforming web pages you visit with custom local scripts written in JavaScript. It's a highly popular and well-developed add-on, which has many other cool uses. For us it will convert plain text file paths in HTML::Mason stack trace to HTML links, clicking which loads files into editor.

To install Greasomonkey, go to its page in Mozilla add-on repository, click "Add to Firefox" and follow the instructions: http://addons.mozilla.org/firefox/addon/748/

Step 2. Add Greasemonkey script to convert stack trace to links

Once Greasemonkey is installed, we need to add our custom script that does the actual transformation. When it is run, text like /path/to/file.html:123 will be turned into HTML: <a href="edit:///path/to/file.html:123">/path/to/file.html:123</a> The script itself can be downloaded here.

To install the script, navigate to your download folder in Firefox and click the downloaded script. Greasemonkey install dialog will open. I haven't found a way to install local Greasemonkey scripts other than via clicking them, even though there used to be a menu item for that in earlier versions of Greasemonkey. Also, script file name MUST end with .user.js, or Greasemonkey will not recognise it.

The code itself is simple enough:

// ==UserScript==
// @name          Mason error source URLificator
// @namespace     http://lvalue.blogspot.com
// @description   Converts source code references in HTML::Mason error page into edit:// protocol links.
// @include       *
// ==/UserScript==

document.body.innerHTML = document.body.innerHTML.replace(/^ +([^ ]+:\d+)<br>$/mg, '<a href="edit://$1">$1</a><br>');

You will probably want to edit the @include property to limit the script to particular URL pattern only, so it doesn't mess up or slow down things outside of your debugging sessions. Here at work, I have it set to: \*.lovefilm.\*, so it includes internal and external websites of the company.

Step 3. Create wrapper script to call your editor

When executing external program as custom protocol handler, Firefox passes it whole URL as single command-line parameter, with protocol prefix included. Since our editor expects correct file path and, separately, a line number, we will perform another simple transformation with the following Perl script:

#!/usr/bin/perl
$ARGV[0] =~ m!^edit://(.+):(\d+)$!s
    or die "Invalid URL: $ARGV[0]";
exec '/usr/bin/emacsclient', "+$2", $1;

You can download this script here.

Step 4. Install wrapper script as edit:// protocol handler in Firefox

WARNING: these instructions apply strictly to Firefox 3.6+. Search the web for help on earlier versions which require a slightly different procedure.

For Firefox 3.6+, Enter "about:config" in your URL bar, press enter, then click the confirmation button. You'll see list a long of settings. Right-click anywhere on it and choose "New", then "Boolean". Enter name: "network.protocol-handler.expose.edit", value: "false", and that's it for editing the settings.

Now the only thing left to do is telling Firefox know what program to run as the editor. You'll do this in configuration dialog that pops up when you first click an edit:// link. Remember, you want Firefox to run our wrapper script, and not your editor directly. If you accidentially chose a wrong program, you can fix it by editing ~/.mozilla/firefox/<profile dir>/mimeTypes.rdf. To make configuration dialog appear right now, click the following dummy edit link Happy debugging of your Mason errors!

2010-11-08

Audio::Metadata 0.14 is relesed

This release finally includes a command-line utility which allows to leverage the power of Unix tools and the piping concept for managing collections of audio files. Only FLAC files are supported, with MP3 support planned for next release. CPAN distribution page http://search.cpan.org/~egorsh/Audio-Metadata-0.14/

Examples

Replace artist in all FLAC files in current directory:

        ametadata read *.flac | sed 's/^artist .*/artist Pat Metheny/' | ametadata update

Write metadata from .cue file to audio files:

        ametadata update_from_cue < album.cue

Edit metadata interactively using default editor (configured with EDITOR environment variable):

        ametadata edit *.flac

2010-08-28

Upload to PAUSE quickly and conveniently

While cpan-upload script that comes with CPAN::Uploader is nice, it doesn't allow to setup default PAUSE credentials, requiring to enter them every time. Another inconvenience is having to manually specify the tarball, which is error-prone if you happen to have a few of them, for different versions. Here's a small script that I wrote to take care of this:

#!/usr/bin/perl

use strict;
use warnings;

use CPAN::Uploader;
use List::Util 'maxstr';
use File::Slurp;


# Path to file containing PAUSE user name and password - as two
# strings, one after another, separated by white space.
my $creds_file_name = "$ENV{HOME}/.cpan_upload";

# Read the credentials.
my %creds;
@creds{qw/user password/} = split /\s+/, File::Slurp::read_file($creds_file_name);

# Find a "*-<X>.<XX>.tar.gz" file with the largest version number in the name.
my $uploaded_file_ext = 'tar.gz';
my $uploaded_file_name
    = maxstr(grep /-\d+\.\d+\Q.$uploaded_file_ext\E$/,
                  glob("*.$uploaded_file_ext"))
        or die "Files matching upload pattern not found in current directory.\n";

# Ask for confirmation.
print "Upload $uploaded_file_name [y/N] ? ";
if (readline(STDIN) =~ /^y\s*$/i) {

    # Carry out the upload.
    CPAN::Uploader->upload_file($uploaded_file_name, \%creds);
    print "Done.\n";
}
else {
    print "Upload cancelled.\n";
}

2010-08-17

Open files specified in error messages on the right line in Emacs

Quite often we see something like this in our logs: [Tue Aug 17 02:29:45 2010] some-script.pl: syntax error at /usr/lib/cgi-bin/some-script.pl line 103, near "; &&" It's easy to move point to file name and call find-file-at-point to open the file, but finding the problem line is usually done manually. The following Emacs Lisp function will parse line number and go to it automatically after opening the file.

(defvar find-file-at-point-with-line-regex
  "Regular expression matching line expression following file name. Must capture line number."
  " line \\(\[0-9\]+\\)")  

(defun find-file-at-point-with-line ()
  "Opens file at point and moves point to line specified next to file name."
  (interactive)

  (let* ((file-name (ffap-file-at-point)) (line-number))
    (if file-name
        (progn
          (while (and
                  (not (string= (buffer-substring (- (point) (length file-name)) (point)) file-name))
                  (not (= (point) (point-max))))
            (forward-char))
          (if (looking-at find-file-at-point-with-line-regex)
              (setq line-number (string-to-number (match-string-no-properties 1))))
          (find-file file-name)
          (if line-number
              (goto-line line-number)))
      (message "No file name at point"))))

Net-Google-Blogger 0.02 released

So I have tested the Moose waters by writing this API. The power of it was evident even in doing most basic OO tasks. It did save significant coding and debugging time. Here's the code, if anyone wonders. GitHub repo is public.

2010-08-16

Switch to special buffers with simple keystrokes in Emacs

Switching to special buffers (usually ones beginning with '*') isn't very convenient using iswitchb (or other switchers, I guess.) But we do switch to them very often.

Here's a piece of Lisp that lets switch to certain pre-configured buffers with just two keystrokes. It can also run a command to create the buffer, if it doesn't exist, and a command immediately after you've switched.

Each special buffer is mapped to a character ("s" for "shell", for example.) To switch, you hit a prefix key first (Hyper-b in the configuration below), then the character key for specific buffer.


(defvar egor/fast-switch-buffers
  '(("s" "*shell*" shell end-of-buffer)
    ("i" "*info*" info)
    ("q" "*SQL*" nil)
    ("g" "*git-status*" git-status)
    ("c" "*scratch*" nil)
    ("m" "*Messages*" nil)
    ("h" "*Help*" nil)
    ("w" "*w3m*" w3m-goto-url)
  "List of buffers switchable via \"hyper-b [key]\". Format: (key buffer-name creation-command
command-after-switch "))


(defun egor/fast-switch-buffer (key)
  "Switches to a special buffer, creating it, if necessary"

  (interactive "k")

  ;; Find respective buffer definition and assign it to lexical variables.
  (let ((buffer-def (assoc key egor/fast-switch-buffers)))
    (let ((buffer-name (nth 1 buffer-def))
          (buffer-command (nth 2 buffer-def))
          (buffer-command-after (nth 3 buffer-def)))

      ;; Err if no such definition is found.
      (unless buffer-def
        (error "No fast-switch buffer definition for key \"%s\"" key))

      ;; Switch to the buffer if it's available. If not, and buffer-creation

      ;; command is defined, run it.
      (if (get-buffer buffer-name)
          (progn
            (switch-to-buffer buffer-name)
            (if buffer-command-after
                (command-execute buffer-command-after)))
        (if buffer-command
            (command-execute buffer-command))))))


;; Bind prefix key
(global-set-key [(hyper b)] 'egor/fast-switch-buffer)

2010-08-02

Import photos from media automatically with Udev, Perl and Gtk2

The following Perl script copies new files from the inserted media to photo library, maintaining hierarchy according to file modification dates: "YYYY/MM/DD/<filename>;".

It's designed to be started automatically upon insertion of new media, by the following Udev rule (adjust device id and paths accordingly):

KERNEL=="sdb1", ACTION=="add", RUN+="/home/egor/bin/update-photos.pl %k /home/egor/Pictures/Photos"

If Gtk2 Perl module is installed, a simple dialog with number of found files and option to skip the import will be presented. The dialog will disappear when the copying is done.

#!/usr/bin/perl

use strict;
use warnings;

use File::Slurp;
use File::Find;
use File::stat;
use File::Basename qw/basename dirname/;
use File::Copy 'copy';
use File::Path 'make_path';
use POSIX 'strftime';


# Become a separate process and wait while storage gets mounted.
exit if fork();
sleep 1;

# Get kernel device name so we can determine mount point, as well as
# directory where to copy to.
my ($device_name, $dest_dir) = @ARGV;
die 'Invalid parameters' unless $device_name && -d $dest_dir;

# Stat destination directory so we can assign everything we create to
# the same owner.
my $dest_dir_stat = stat $dest_dir;

# Try loading Gtk2 library and set success variable.
eval 'require Gtk2';
$ENV{DISPLAY} ||= ':0.0';
eval { Gtk2->init };
my $with_gui = !$@;

# Set testing/debugging variables.
my $force_copy = 0; # copy files even if they're there already
my $is_dry_run = 0; # work as normal, but don't do make actual modifications

# Unbuffer STDOUT output.
$|++;

# Determine mount point for the inserted media.
my ($src_root_dir) = read_file('/proc/mounts') =~ /^\/dev\/$device_name\s+(\S+)/m;

# Find new files.
my @new_files;
find(
    sub {
        return unless -f $File::Find::name; # skip directories

        my $dest_path = file_name_to_dest_path($File::Find::name);
        return if !$force_copy && -f $dest_path;
        push @new_files, [ $File::Find::name, $dest_path ];
    },
    $src_root_dir
);

# If GTK has been loaded, display a dialog for information or confirmation.
if ($with_gui) {

    # Display different buttons depending on whether we've found anything.
    my @buttons =
        @new_files ?
            ('Copy' => 'ok', 'Not now' => 'cancel') :
            ('OK' => 'ok');

    # Create dialog with appropriate buttons and size.
    my $dialog = Gtk2::Dialog->new_with_buttons(
        'New photos found',
        undef,
        [ 'modal' ],
        @buttons
    );
    $dialog->resize(200, 100);

    # Display number of files found.
    my $label = Gtk2::Label->new((@new_files || 'No') . " new files found");
    $label->show;
    $dialog->get_content_area->add($label);

    # Display the dialog and respond to user choice.
    my $response =  $dialog->run;
    exit if !@new_files || $response eq 'cancel';
}
else {
    exit unless @new_files; # if no new files found, do nothing
}

# Copy each new file, setting the owner to owner of top destination directory.
foreach (@new_files) {

    my ($src, $dest) = @$_;
    copy_new_file($src, $dest);
    chown $dest_dir_stat->uid, $dest_dir_stat->gid, $dest;
}


sub file_name_to_dest_path {
    ## Returns full destination path for the given file name. The path
    ## has the format of 'YYYY/MM/DD/<file_name>'.
    my ($file_path) = @_;

    my $modif_time = stat($file_path)->mtime;
    return strftime "$dest_dir/%Y/%m/%d/" . basename($file_path),
                    localtime $modif_time;
}


sub copy_new_file {
    ## Copies new photo file, creating destination directory, if necessary.
    my ($src_path, $dest_path) = @_;

    # Make sure destination directory exists.
    my $dest_dir = dirname($dest_path);

    unless (-d $dest_dir) {
        print "Creating $dest_dir ...";

        make_path($dest_dir) unless $is_dry_run;
        chown $dest_dir_stat->uid, $dest_dir_stat->gid, $dest_dir;

        print "Done.\n";
    }

    # Copy the file.

    print "Copying $src_path to $dest_path ... ";

    copy($src_path, $dest_path) unless $is_dry_run;
    chown $dest_dir_stat->uid, $dest_dir_stat->gid, $dest_path;

    print "Done.\n";
}