Friday, September 24, 2010

Console2 and Altap Salamander: together at last!

Introduction

Not unlike Nuts and gum, I wanted Altap Salamander to play nice with Console2 by having the former launch the latter whenever I hit the / key above the numeric keypad. Console2 is awesome: it supports using Courier New as a font, has much more natural text selection, features copy & paste keyboard shortcuts, etc.

The problem

I used to connect the two together by launching Salamander with a batch file that would override the COMSPEC environment variable to point to Console2, which mostly worked, until a process launched by Salamander itself would try to use a shell. It turns out Console2 is NOT a shell and thus I would often get really weird behaviour out of those programs.

The solution, part 1

It turns out an Automation plug-in now exists for Salamander. Using it, I wrote the following quick script, adapted from the Launch Elevated Command Prompt.vbs sample script:

Set ShellApp = CreateObject("Shell.Application")
ShellApp.ShellExecute "E:\Program Files (x86)\Altap Salamander 2.54\Console2\Console.exe", "-d """ & Salamander.SourcePanel.Path & """", ""


The Plugins Manager allows me to bind keyboard shortcuts to individual scripts, so I selected Alt+- since just about everything else was taken and you can't use a shortcut key that Salamander itself already uses. A few dialog dismisses later and Alt+- indeed opens up Console2 to the current panel's path. Not bad, but my brain has fused the numpad / as the go-to key for opening a command prompt, so I need to fix that, too.



The solution, part 2



Another thread in the Salamander forums suggested using AutoHotKey to intercept NumpadDiv when Salamander has focus and using it to launch Console2. That kind of works, minus having Console2 launched at the current panel's folder. Well, remember the shortcut key I gave my automation script earlier? I can get AutoHotKey to intercept the numpad / and instead emit Alt+- when Salamander has focus:



#IfWinActive,ahk_class SalamanderMainWindowVer25
NumpadDiv::SendEvent !-
RETURN

Kazaam! Shazam!

Conclusion



All of this brouhaha could have been avoided if Salamander would simply let me configure what gets launched when I hit numpad /, hint, hint, nudge, nudge.

Thursday, June 24, 2010

Don Norman at Business of Software 2009

There's an excellent video at Neil Davidson's blog of Don Norman talking about successful products and services, which he summarizes to the following ten points (1 and 10 are meant to be the same):
  1. It's all about the experience
  2. Design systems
  3. Everything is a service
  4. Everything is a product
  5. Don't be too logical
  6. Memory is more important than actuality
  7. Complexity is good: Complicated is bad
  8. Design for the real world
  9. Design for people
  10. It's all about the experience

Tuesday, June 22, 2010

A case for code reviews

THIS is a fabulous example why your organization/team should be performing code reviews.

We're not talking about poring over the entire code base once a month (that's just cruel); no, you check the code your teammate or colleague wrote or modified before it even goes into the main branch of the source control repository, just like how open-source projects do it.

Use Review Board: it's free and works with the modern source control systems. If you have one of the source control systems that isn't supported, have one of your teammates come to your desk to give your changes a once-over until you figure out a software solution that allows you to send them the changes so they can review it on their own time while you start working on something else.

Seriously. Stop the bad software madness in its tracks TODAY.

Monday, May 24, 2010

The coolest code

At [the appropriately-numbered] revision 42 in a certain source control repository lies, without a doubt, the biggest masterpiece of software ever written since the advent of the parentheses:
public static IEnumerable<T> PreOrder<T>(this T startingPoint, Func<T, IEnumerable<T>> children)
{
    yield return startingPoint;
    foreach (var child in children(startingPoint))
    {
        var preOrderedChildren = PreOrder(child, children);
        foreach (var preOrderedChild in preOrderedChildren)
        {
            yield return preOrderedChild;
        }
    }
}
"What is it?" you say? It's a generic, recursive generator, implemented as an extension method with a functor.

"Uhh, so... what does it do?" you counter? It traverses a tree of items (of type T) by yielding them, starting at the provided startingPoint and obtaining the children of a given instance of T using the provided children function object.

"I am from Missouri. You have got to show me." Sure thing! Suppose we have this Node class:
public class Node : IEnumerable<Node>
{
    private readonly IList<Node> _children = new List<Node>();
    public IEnumerable<Node> Children { get { return _children; } }

    private readonly string _name;
    public string Name { get { return _name; } }

    public Node(string name)
    {
        _name = name;
    }

    public Node Add(string nodeName)
    {
        return Add(new Node(nodeName));
    }

    public Node Add(Node node)
    {
        _children.Add(node);
        return node;
    }

    #region IEnumerable<Node> Members
    public IEnumerator<Node> GetEnumerator()
    {
        return Children.GetEnumerator();
    }
    #endregion

    #region IEnumerable Members
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return Children.GetEnumerator();
    }
    #endregion
}
...we can then use the Node class to represent its own high-level parse tree:
var compilationUnit = new Node("Node.cs")
{
    new Node("namespace Test")
    {
        new Node("public class Node : IEnumerable<Node>")
        {
            new Node("public IEnumerable<Node> Children")
            {
                new Node("get;")
            },
            new Node("public string Name")
            {
                new Node("get;")
            },
            new Node("public Node(string name);"),
            new Node("public Node Add(string nodeName);"),
            new Node("public Node Add(Node node);"),
            new Node("#region IEnumerable<Node> Members")
            {
                new Node("public IEnumerator<Node> GetEnumerator();"),
            },
            new Node("#region IEnumerable Members")
            {
                new Node("System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator();"),
            },
        },
    },
};
Notice how the Node constructor accepts both regular arguments (in this case, a string representing the node's Name) as well as a list of Node instances? That feature -- collection initializers -- is made available to classes that implement IEnumerable as well as an Add method.

The PreOrder method can then be called on the compilationUnit instance, as follows, due to the extension method feature:
var sequenceOfNodes = compilationUnit.PreOrder(n => n.Children);
The second argument to the method is a lambda expression that, given an instance of Node, returns an IEnumerable<Node>. In other words, it explains to PreOrder how to get a sequence of Node instances given a single Node instance. In our case, it is rather simple, as the Node class has the Children property for that purpose (and it could have been even simpler than that, since Node implements IEnumerable and thus the second parameter could have been written as n => n).

Anyway, calling the PreOrder method looked like it did nothing and it almost did indeed do nothing, which is the point of a generator: until you start pulling on the IEnumerable, no work is performed and no items are generated. All that was done by calling PreOrder was setting up the generator instance in the sequenceOfNodes variable. Let's actually start generating (a.k.a. yielding):
foreach (var node in sequenceOfNodes)
{
    Console.WriteLine(node.Name);
}
...when that loop starts executing, the code in PreOrder kicks in and the first item yielded is the startingPoint, which was initialized in compilationUnit, so its Name is printed to the console. The children of startingPoint are obtained by calling the children functor on startingPoint itself. As you will remember, that's simply the Children property. The process repeats recursively behind the scenes, yielding a node, then its children, while our loop doesn't need to worry about all of that. The loop will end up printing a flat list version of the original tree.

Conclusion (a.k.a. Too long; didn't read)

In 12 lines of code, I made use of the following groovy C# 3.5 compiler features:
  1. Extension methods: extend a closed type with a static method that appears like an instance method
  2. Lambda expressions: inline, anonymous methods that replace private classes that implement an interface AND can operate on local variables
  3. Generators (a.k.a. Iterators): the yield return keyword in methods that return IEnumerable
  4. Implicitly-typed local variables: the var keyword, to avoid repeating yourself

Bonus

If this modest display of mad skillz hasn't convinced you to switch to .NET 3.5, well, you don't even need to! You can compile all this code with the C# 3.5 compiler but still target the .NET 2.0 runtime or even the JVM!

Targetting the .NET 2.0 runtime

Not only can you make use of the new compiler features, you can also make use of the new IDE features, such as call hierarchy and reference highlighting.
  1. Open Visual Studio (this should work in Visual Studio 2008 and 2010)
  2. File > New > Project...
  3. Select .NET Framework 2.0 from the drop-down list on the right:
  4. Create the ExtensionAttribute replacement by adding a file called ExtensionAttribute.cs in your project with the following contents:
    namespace System.Runtime.CompilerServices
    {
       public class ExtensionAttribute : Attribute { }
    }
    
  5. Create a Delegates.cs file that contains the missing Action and Func delegates from the System namespace (you probably only need up to 4 arguments)
  6. Start writing cool code!

Targetting the JVM

This one is more complicated, but the tool you need to download is Mainsoft Grasshopper. You'll need to perform similar additions of missing attribute and delegates as above, but then you should be fine.

Wednesday, May 19, 2010

Arrange, Act, Assert

There's a pattern in unit test writing that I noticed a few years back, but it wasn't until recently that I discovered this pattern actually has a name: Arrange, Act, Assert. These represent the three terrifying waves distinct phases of a good unit test: the first part prepares the necessary conditions that simulate a scenario or use-case (arrange), the second invokes the functionality being tested (act) and the third checks that some post-conditions hold (assert).

Here's some Java code I was writing at the time I first noticed the pattern (November 2007):
/**
 * Tests the <i>shuffle</i> method against the Collections.shuffle(List<?>,
 * Random) implementation from which it was derived.
 */
@Test
public void shuffle_AgainstReference ( )
{
    // { initialization
    Random randomSource;
    int length = 20;
    double[] sourceArray = new double[length];
    List<Double> expectedList = new ArrayList<Double> ( length );
    for ( int i = 0; i < length; i++ )
    {
        sourceArray[i] = i;
        expectedList.add ( (double) i );
    }
    // }

    // { double-check
    for ( int i = 0; i < length; i++ )
    {
        String message = "Source array is different at index [" + i + "]";
        assertEquals ( message, expectedList.get ( i ), sourceArray[i] );
    }
    // }

    randomSource = new Random ( 42 );
    ArrayUtil.shuffle ( sourceArray, randomSource );

    randomSource = new Random ( 42 );
    Collections.shuffle ( expectedList, randomSource );

    // { validation
    for ( int i = 0; i < length; i++ )
    {
        String message = "Shuffled array is different at index [" + i + "]";
        assertEquals ( message, expectedList.get ( i ), sourceArray[i] );
    }
    // }
}
That code tests that my implementation of ArrayUtil.shuffle() on an array of doubles works just like the implementation of Collections.shuffle(). One will notice that I called the first block or phase "initialization" and the last one "validation" (which, come to think of it, should have been called "verification" -- more on this at Wikipedia). The block labeled "double-check" should probably have been taken out into its own test.

I recently noticed the more formalized use in the NamedStringFormatSolution.zip project (more about this project at Phil Haack's Named Formats Redux blog post), where the 3 phases of unit testing were explicitly called out by comments in the code:
[Fact]
public void Eval_WithNamedExpressionAndFormat_EvalsPropertyOfExpression()
{
    //arrange
    var expr = new FormatExpression("{foo:#.##}");

    //act
    string result = expr.Eval(new { foo = 1.23456 });

    //assert
    Assert.Equal("1.23", result);
}
The arrange phase is sometimes so trivial that its contents is folded into the act phase, but unless a value is repeated in several tests that it becomes cleaner or less error-prone to extract it out in a constant, it should remain in the test for maximum clarity.

So there you have it: the next time you write a test, design it to execute in three distinct phases of arrangement, acting and assertion. Your test will be better designed, easier to read and other maintainers will thank you for it.

Saturday, April 10, 2010

Reformulate to avoid misunderstandings

Some of the best advice on technical writing that I have ever read suggested the writer try to explain their ideas in more than one way, usually through the magical phrase in other words.

The technique works by introducing your topic in one writing style, vocabulary, point of view, abstraction level, etc. and then the following sentence conveys mostly the same information, but from a different angle, using synonyms, using more/less details and/or introducing requirements or consequences of the idea. (See how I did that?)

It may seem like it is adding superfluous redundancy, but it helps avoid ambiguity and the clever use of synonyms should help make your document easier to find by search engines.  Just be careful not to overuse the technique.  In other words, some ideas are simple enough that they do not need to be restated.

Thursday, April 08, 2010

Jamie Cansdale is my hero

Jamie Cansdale (of Mutant Design) is the author of TestDriven.Net, a Visual Studio add-in that "makes it easy to run unit tests" (and it certainly lives up to that description).  A simple, casual feature request sent by e-mail two days ago was followed by fabulous customer support that culminated today in a private build that fully implements the feature, even with our unusual environment at work.

Jamie's e-mails came in rapidly and always with offers for more help in getting me to better use his already-fantastic product.  I sometimes had trouble keeping up with him!  The conversation was to the point and I felt I was being treated as a very important customer, not just some [l]user who needs to be clued in to the basics of computers by some first tier customer support who gets paid minimum wage and doesn't care about their job.

Jamie and I iterated through 6 private builds first by e-mail and then by instant messaging to explore the requirements and smash all the bugs in the implementation so that TD.Net could fully support the DeploymentItemAttribute, just like MSTest, even when the files being referenced were marked as read-only due to the nature of the source control system in use.  I came out of this experience a very satisfied customer, as - even though I use the software as a student to write open-source software - I decided to put my money where my mouth is and purchased a professional license.

If you're still reading, what are you waiting for?  Go buy the product and/or go provide customer service so good that your users will write blog posts about it!

Monday, November 09, 2009

How to secure your wireless network

Note: The following is a repost of how-to of the same name I wrote tonight at my IT notes site.

Intro

Do you have a wireless router and a wireless device and want to make sure your neighbours aren't freeloading your internet connection or snooping through your network? Follow these instructions to lock down your wi-fi.

Get what you need

  1. A router that has both wired ports and wireless capability.
  2. A computer with a wired connection to the router.
  3. A computer with a wireless network connection (it could be the same computer as in #2).
  4. A piece of paper large enough to write 5 lines of text on it. You will be filling in the following template:
    IP Address


    User Name


    Password


    SSID


    Passphrase

  5. A pen.
  6. Some adhesive if the paper in #4 did not have its own.

Let's get started!

  1. Plug your computer into the router (and switch off or unplug the wireless network adapter if your computer has both). This makes sure you're configuring the right router (and not your neighbour's) as well as gives you a way to fix it if you accidentally lock yourself out. Some routers only allow you to configure them through a wired connection anyway.

  2. Find out what your gateway's IP Address is. It will be something like 192.168.0.1 but it depends on the manufacturer and model, as well as the initial set-up.

    • Windows XP:
      1. Start > Control Panel
      2. Double-click on Network Connections
      3. Double-click on the Local Area Connection icon
      4. Click on the Support tab
      5. Write down the numbers that are next to Default Gateway as the IP Address.

    • Windows Vista and 7:
      1. Show the Start menu
      2. Type network connections in the search box and select View network connections
      3. Double-click on the Local Area Connection icon
      4. Click on the Details... button
      5. Write down the numbers that are next to IPv4 Default Gateway as the IP Address.

  3. Open your web browser and in the address bar, type http:// followed immediately by the gateway's IP address. For example: http://192.168.0.1

  4. You should be prompted for a user name and password. If you have never set this, then you have a few options for figuring out what the default user name and password are:

    • Find the little user's manual that came with the device. It should have a section on administration or remote access. Worst case, you flip through the 10-20 pages looking for either an image similar to what's on your screen right now or some mention of any of the following words: admin, login, password or user name.

    • Find a digital version of the user's manual:

      1. Go to the manufacturer's website. If it's not printed on the router anywhere, search the web.
      2. Navigate to their Support section of the website.
      3. Look for a Product or Model Search and type in the model number of your router.
      4. One of the links for your router's page (when you eventually find it) should be for the manual, usually as a PDF file.

    • Guess! Here are some common combinations:

      1. user name admin and no password
      2. no user name and admin as the password
      3. user name admin and password admin
      4. no user name and no password

    • Now that you have found the default user name and password, you should change them as soon as possible. Look under Tools, Advanced or Maintenance. Invent a password you don't use for anything else and write both the User Name and the Password on your piece of paper.

  5. Look for a Wireless or WLAN menu. You will know you have found the right page when you see words like SSID, WEP, WPA, Passphrase and Channel.

  6. Your Network Name is commonly called SSID. You change this from its default of default (or sometimes it's the manufacturer's name) and set it to something that's easy for you and your guests to know it's your router. Keep it simple and a single word if possible, like the name of a pet. Whatever you choose, write it down on your piece of paper.

  7. The next part is a little trickier. You usually need to select what kind of security you want. If you're reading this, you want to change it from None or Disabled to WPA.

    • Side note: Avoid WEP at all costs as it is too easy to break in. Protecting your wireless network with WEP is like putting a sign in front of a pool, kindly asking people not to go swimming. Protecting your wireless network with WPA is like building a fence around your pool. The most determined swimmers can still climb your fence, but casual swimmers that would not have been deterred by the "please don't swim" sign will go look for easier pools to break into.

    • Side note number 2: Your router might allow you to select WPA2. In my experience, this protocol is not as compatible with as many devices, so I would not recommend it.

  8. Now you get to pick a Passphrase (also known as a shared key). A passphrase is like a password, but it's longer and it can contain spaces. Pick an easy to spell one-liner that you and your guests can easily type, something like I am one with everything. It usually needs to be at least 8 characters long and, just like a password, it will be case-sensitive. When you have typed it twice, click whatever Save button there is and you're almost done!

  9. At this point, your router might want to think for a few seconds or maybe it will automatically reboot. In any case, wait 10-30 seconds and then try connecting to your router using a wireless-capable device. If you are using the same computer, unplug the network cable and then switch on (or plug in) your wireless network adapter. After another 10-15 seconds, you should get a message saying some wireless networks were detected and to click to see a list. You should see your newly-named wireless network in that list with a little padlock indicating it needs a key. If your network is not listed, you can wait another minute or two and then start over in case you got something wrong along the way.

  10. It works! Yeehaw! Now attach your piece of paper under your router using some light adhesive so that the next time you need to fix something, you will be able to skip steps 2 and 4. It will also be useful when you want to give your guests access to your wireless network; you won't have to try to remember anything, you can just lift up your router and read off the SSID and Passphrase to them.

Wednesday, August 19, 2009

HOWTO: Shop for a laptop

Note: The following is taken verbatim from an e-mail I sent a relative who was interested in purchasing a laptop. It's reposted here so I can share it with the world and subsequently link to it the next time I'm asked about it. That and some feedback from the public at large couldn't hurt!

The best way I found to shop for laptops is to find as many flyers as possible and cut them apart so you have playing-card sized pieces with the picture of the laptop, its price and its specs. The Future Shop and Best Buy flyers are great for this and sometimes the Dell, The Source and Staples flyers are useful too.

You then start to whittle it down by eliminating obviously too-expensive laptops and too-low end laptops. (i.e. those in the $3000 range and those with less than 2 GB of RAM) You just toss the pieces in the garbage. For the next iteration, you start looking more carefully at the specifications, pictures, etc. and tossing out those that don't fit your wants and needs. Repeat until you have about two or three left.

What you do next is you go to the stores, find where the two or three you have left are, and play with them for 5-10 minutes to see if there's a clear winner. If you can't tell the difference between the them, well, the least expensive wins. I recommend you sleep on it at this point.

Laptops usually come with a one-year warranty and you can purchase a few more years after that for up to 3 or 4, depending on the store. That can easily add $300 to the cost of the laptop, so you may want to decide on that before you even step into the store, otherwise you'll be asked to decide at check-out and making a decision about something that's almost 1/3 the cost of the product is not something you want to do quickly.

I would not recommend purchasing a laptop (over a desktop) unless the computer needs to be mobile and moved around a lot. You'll pay more for the privilege of having small pieces that fit inside a portable package and thus the same money could be spent on something more powerful with a bigger screen, etc.

Wednesday, April 22, 2009

On uninstalling Google Chrome

I finally gave up with Google Chrome on my laptop. Here's what popped up when I completed the uninstallation process, with my answers:

Google Chrome has been uninstalled.

Thanks for trying out Google Chrome. Please help us improve Google Chrome by telling us why you uninstalled it. Your feedback is much appreciated.

Which of the following best describes you?
I am uninstalling Google Chrome for now, but I may try it again in the future
I am permanently uninstalling Google Chrome
I am only uninstalling briefly. I am going to reinstall straight away (e.g. changing computer, upgrading operating system)

Why are you uninstalling Google Chrome? Please check all that apply
It doesn't load some web pages properly (please list any examples below)
It's missing some features that I use (please provide detail in the box below)
I'm concerned about privacy (please provide detail in the box below)
It seems to slow down my computer
It crashes too often
Other (please provide more details in the text box below)

Please provide any additional detail on your reason for uninstalling:

There are too many Firefox add-ons that I have come to rely on and using Chrome just leaves me "naked" in comparison. I blogged about this potential problem when Chrome initially came out: http://oliiscool.blogspot.com/2008/09/world-in-verdana.html

I first installed Chrome when I heard Gmail and Google Reader had exceptionally good performance in Chrome. This was true, but as soon as I clicked a 3rd-party link from within either, I felt I was abandoned and frequently would end up copy/pasting the URL into Firefox and continue from there.

I definitely appreciate the engineering skills and value of having a process-per-tab kind of browser, but that functionality alone is not sufficient for a heavy add-on user such as myself. In fact, I have checked the "slow down" checkbox since that model seems to in fact be a liability when viewing YouTube videos: I definitely notice much higher CPU usage from Chrome when visiting a site containing an embedded video than visiting the same page with Firefox.

Perhaps not all is lost: the process-per-tab model could still be applied to Firefox and a "Chromium Tab" Firefox add-on that worked similarly to the "IE Tab" add-on (in that it would enable the Webkit/Chromium engine on a per-tab basis) would be fantastic in my books.



So maybe I'll eventually re-install it. There's definitely value in the "application mode" as well, which hides the address bar and makes a "tab" its own window using the site/application's "favico" as the application window's icon, but that breaks quickly - as I described above - when you need to go outside that site/application. That "application mode" would also be a neat add-on/extension for Firefox. :)

Friday, April 03, 2009

Do you still not use a router?

I was helping out an extended family member over the phone the other night when I realized he would connect one of his two computers directly to the cable modem, in turn, depending on which computer he was going to use to get internet access.

This struck me as being very odd. Ten years ago, I was using hubs and later migrated to switches and then router/switch combinations to network computers. I just can't imagine what life would be like to have more than one computer in a house and not some sort of network between them.

More scary was that he called me on April 1st, the day the Conficker author(s) decided to freak everyone out. Since Conficker's primary infection mechanism was to exploit a vulnerability in a Windows service and he had just reinstalled Windows on that computer, I was worried he could get infected and warned him about the dangers of doing so.

In this case, a router would have not only removed the "unplug this computer, plug that computer" inconvenience but also have acted as a firewall between his computer(s) and the internet, therefore making it difficult to get infected by worms like Conficker just because your computer is connected to a [hostile] network.

Thankfully, I have an extra router lying around that I'll give him the next time I see him, but seriously, if you don't have a router, go spend $50 on a networking device that doubles as a firewall. Most internet software is NAT-aware these days, so you're no longer trading off convenience for security.

Monday, February 02, 2009

Ego-driven software development

(or "How M.C. Escher Would Have Packaged His Software")

This was too "good" to not blog about. I heard about IronPython Studio, a Visual Studio-based IDE for writing Python code for/with .NET tools. In order to install this interesting gem, you first need to install its pre-requisites, which is one of flavors of the "Visual Studio 2008 Shell": Isolated Mode or Integrated Mode. Sounds easy, right? Wrong!

The actual downloads seem innocent enough (I got both, just to be safe -- as an aside it was hard to tell which one would suit me best): they arrive as executables. Here's how many levels of "packaging" there are:
  1. Running either of vs_AppEnvRedist.exe or vs_ideredist.exe will create a temporary directory where the EXE's files are extracted and an "installer" is launched

  2. You accept the EULA and click next a few times and what do you end up with? "The redistributable package has been installed". That's right, you ran an installer that installed another installer. Total disk space needed for this (at apogee): 400 MB for the original download, 400 MB for the temporary files and 400 MB for the "redistributable package" = 1200 MB

  3. As you finish the first "installer", the temporary files are cleaned up, so we're back to consuming 800 MB. You run the second installer and the first thing it does is check its signature, which consumes 400 MB of RAM. It then proceeds to extract files to another temporary directory (400 MB again, although this time it's in a bunch of smaller files - 286 MB of which is various versions of the .NET framework), thus bringing our used disk space back up to 1200 MB. Those of you following at home will notice that I haven't actually installed anything useful yet. Accept another EULA, select the only feature (wut?), pick the destination folder and go (again)! At apogee: 400 MB + 400 MB + 400 MB + whatever installed size it was (I didn't check)

  4. Oh, it looks like we're actually done! What was I installing, again?

I'm reminded of Adobe Acrobat Reader installers from, oh, I don't know, ten years ago, before one-file installers were even invented. IronPython itself is available in a single MSI file, while IronPython Studio's download options are both available as an MSI file in a ZIP file.

There is no technical reason for this. There simply is no excuse for these fractal installers except that someone (or an entire team of someones) at Microsoft decided they needed to be involved in the supply chain that brings us internauts this bare Eclipse-wannabe that does not even include a text editor. I mean, seriously, the only thing that could have been worse would have been to wrap the whole thing in a "downloader" (like Visual Studio Express) or in an ISO 9660 file (like Visual Studio 2008 Service Pack 1).

I have a special offer for the person in charge of the Visual Studio team: I will personally come over and deliver atomic wedgies to everyone responsible for these shenanigans! Just give me a call; you know how to find me.

There is some good news after all this: IronPython Studio not only just works, but so does its debugger. Kudos to that team.

Saturday, October 25, 2008

Apple sucks at XML

OK, I've officially had it with Apple. Steve Jobs may have style down cold, but his programmers were smoking something fierce when they designed the XML format for their so-called Property list. Don't let that Wikipedia page fool you on the apparent simplicity of the format. Take a look at one of Apple's own samples. Still not convinced? How about a real-world use-case: the emoticon definition file for an Adium theme, a portion of which is reproduced below:
<plist version="1.0">
<dict>
<key>AdiumSetVersion</key>
<integer>1</integer>
<key>Emoticons</key>
<dict>
<key>amazing.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>=-o</string>
<string>=-O</string>
<string>:-o</string>
<string>:-O</string>
</array>
<key>Name</key>
<string>Surprised</string>
</dict>
<key>anger.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>&gt;:o</string>
<string>:-@</string>
<string>:@</string>
<string>X(</string>
</array>
<key>Name</key>
<string>Angry</string>
</dict>
<key>bad_egg.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>&gt;-[</string>
<string>&gt;-(</string>
</array>
<key>Name</key>
<string>Nervous</string>
</dict>
(...snip...)
</dict>
</dict>
</plist>
Do you see what the problem is? For those of you playing at home, here's a hint: how would you write an XPath expression to obtain the "equivalents" of a given image file?

Yes, it's not impossible to grab a value for a given key, but did they have to make it so hard when XML can express the same idea in a much easier format? Or, rather, did they have to be so lazy when writing the code that serializes these property lists to/from XML?

In any case, if you ever have the need to process an XML file created by an Apple program, the following stylesheet will (likely) help restore your sanity. Simply pre-process the XML with my stylesheet and then your XML code or stylesheet will be much easier to write (and read!):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

<xsl:output method="xml" encoding="utf-8" indent="yes" />

<xsl:template match="* | @* | node()">
<xsl:copy>
<!-- if the previous sibling is a 'key' element -->
<xsl:if test="name(preceding-sibling::*[position()=1]) = 'key'">
<xsl:attribute name="key">
<xsl:value-of select="preceding-sibling::key[position()=1]/text()" />
</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="* | @* | node()" />
</xsl:copy>
</xsl:template>

<xsl:template match="key" />

</xsl:stylesheet>
For an example, let's take another look at the sample XML I showed earlier and compare that with the XML sexiness that is generated by applying my stylesheet against it (some spacing was added to the "after" version to better illustrate how they compare to each other):
BeforeAfter
<plist version="1.0">
<dict>
<key>AdiumSetVersion</key>
<integer>1</integer>
<key>Emoticons</key>
<dict>
<key>amazing.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>=-o</string>
<string>=-O</string>
<string>:-o</string>
<string>:-O</string>
</array>
<key>Name</key>
<string>Surprised</string>
</dict>
<key>anger.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>&gt;:o</string>
<string>:-@</string>
<string>:@</string>
<string>X(</string>
</array>
<key>Name</key>
<string>Angry</string>
</dict>
<key>bad_egg.png</key>
<dict>
<key>Equivalents</key>
<array>
<string>&gt;-[</string>
<string>&gt;-(</string>
</array>
<key>Name</key>
<string>Nervous</string>
</dict>
(...snip...)
</dict>
</dict>
</plist>
<plist version="1.0">
<dict>

<integer key="AdiumSetVersion">1</integer>

<dict key="Emoticons">

<dict key="amazing.png">

<array key="Equivalents">
<string>=-o</string>
<string>=-O</string>
<string>:-o</string>
<string>:-O</string>
</array>

<string key="Name">Surprised</string>
</dict>

<dict key="anger.png">

<array key="Equivalents">
<string>&gt;:o</string>
<string>:-@</string>
<string>:@</string>
<string>X(</string>
</array>

<string key="Name">Angry</string>
</dict>

<dict key="bad_egg">

<array key="Equivalents">
<string>&gt;-[</string>
<string>&gt;-(</string>
</array>

<string key="Name">Nervous</string>
</dict>
(...snip...)
</dict>
</dict>
</plist>

...isn't that a sight for sore eyes? You're welcome.

Wednesday, September 03, 2008

The world in Verdana

OK, by now everybody and their cat has heard about Google Chrome. The general buzz on Slashdot and blogs seems to be "it's not Firefox" and, more specifically, a general lament of the chicken and egg problem surrounding the release of a new platform when there isn't any software (in this case "plug-ins" or "add-ons") to run on it. Well, OK, there are some Chrome plug-ins available, but Chrome extensions are currently NOT supported. On the other hand, the source code is out there and given that it has the "Google brand", it won't be hard to find motivated geeks hacking some neat software to [eventually] bring Chrome on par with Firefox, with said geeks' hidden agenda of being noticed by Google and be offered a nice googlejob where they get to sit on their googlechair, etc.

It's technically not such a bad chicken-and-egg situation, with the massive and thorough testing Google [claims to] have performed. Plus, the browser is more than a fine replacement for users of Internet Explorer or plain, out-of-the-box Firefox, thus making all us geeks appear crazy when non-techies ask us why we're not using Google Chrome, when we use everything else Google throws at us.

Anyway, I didn't set out to write yet another review, but to post solutions to problems I encountered:

Installing Chrome as a non-administrator


I hit issue 119 ( Install Fails on W2K8 with low-rights user ) when I tried to install on my computer as a non-administrator and got the following:



Thankfully, the work-around by stephen.oakman in comment 6 worked and I found the elusive chrome_installer.exe in a folder matching the pattern C:\Documents and Settings\[user]\Local Settings\Application Data\Google\Update\Download\[guid]\chrome_installer.exe and was able to install it successfully.


Setting the minimum font size


I also had the same accessibility lament about minimum font sizes and font family overrides. For example, here is the Getting Started page in Firefox 2, with Verdana @ 14pt bliss:



...and this is what Chrome gave me with the same page:



OK, yes, the Firefox version looks weird with the title text not lining up with the logo, etc. but all the content is perfectly legible, which is more important to me. Well, more legible than the fonts picked by the web developer.

Thankfully, I have solved the first half the problem with a few quick searches in the source code and now my C:\Documents and Settings\[user]\Local Settings\Application Data\Google\Chrome\User Data\Default\Preferences file contains this little gem:


"webkit": {
"webprefs": {
"cursive_font_family": "Verdana",
"default_fixed_font_size": 14,
"default_font_size": 14,
"fantasy_font_family": "Verdana",
"fixed_font_family": "Courier New",
"minimum_font_size": 14,
"minimum_logical_font_size": 14,
"sansserif_font_family": "Verdana",
"serif_font_family": "Verdana"
}
}



...which gives me a slight improvement in readability in Chrome:



You can find out what the names of the [other] supported hidden preferences are by peeking into chrome/common/pref_names.cc and cross-referencing with WebContents::GetWebkitPrefs() in chrome/browser/web_contents.cc. In particular, you'll find (as of this writing) that the other half of my problem is already identified in a comment:



// User CSS is currently disabled because it crashes chrome. See
// webkit/glue/webpreferences.h for more details.



...with the more details being:



// TODO(tc): User style sheets will not work in chrome because it tries to
// load the style sheet using a request without a frame.
bool user_style_sheet_enabled;
GURL user_style_sheet_location;



D'oh. Maybe this will inspire someone else to fix that part of the code or otherwise provide the elusive "let me choose my own damn fonts" setting that I rely on for keeping my sight and posture in good shape.

Friday, August 08, 2008

Effort vs. Results

I remember having a conversation with my father when I was a kid about his company's employees. I was shocked to discover they were being paid by the hour. I remember following up with a question along the lines of "Wouldn't that encourage them to take lots of time to do their work?" He assured me that, although it was a possibility, it didn't happen very often. (and presumably he explained that people who did that could lose their job, so it was to their long-term advantage to not slack off)

Fast forward a few years later, when I'm in high-school and a student asks the teacher - after she explained a homework assignment - if any marks would be given for effort. I couldn't help but laugh out loud, thinking he was pulling the teacher's leg. He didn't join me in laughing or smiling (and probably shot me a dirty look). Uh oh. He was serious!?!?

Maybe I found that proposition silly because I figured there was no way for it to be reliably measured: it would have to be self-reported. How hard would it be to say "I spent 100 hours on this" when handing it in? And if I had completed the assignment in 10 hours and produced equivalent results to his, would that mean he would get more marks than I got because he spent more time?? Worse yet, even if it wasn't self-reported, how would it be measured??? And what about the difference between "brain time" and "body time"?

Nowadays, I understand why people are [usually] compensated by how much their skills are in demand and that it is possible to be 10 times better than someone else at what you do. Maybe I also did as a kid? That might explain my reaction in both instances. Could it also explain my drive for correctness? My passion for getting things done and done right?? Now here's a doozy for you: would this knowledge at a younger age have helped other students??? Or maybe I'm just being arrogant and that I should just shut up for being the nerd that didn't have any trouble with his assignments????

Discuss.

P.S.: Please go easy on me as I have been working on this blog post since November and it's only now that I have been able to finish it.

Saturday, July 26, 2008

I got in a "jam"...

Round 1A of Google's Code Jam programming contest (in which I participated) ended about an hour-and-a-half ago (there are three sub-rounds of Round 1 and contestants can compete in two of them to try to move on to Round 2).

A failure (on my part) to pay close attention to the requirements of the first problem meant I implemented an algorithm with a complexity of O(n! * n!), which means 25 401 600 iterations for n = 7 (sort of reasonable) but 1 625 702 400 iterations for n = 8, something my laptop wouldn't be able to finish in any reasonable amount of time. It's much worse when you consider that n was expected to go as high as 800! Once I read that, it occurred to me that I had been going at it entirely the wrong way... About an hour-and-a-half of going the wrong way, which involved implementing (and debugging) a nice "permute the items of this list" method.

You see, there was a trick to the problem. Once I realized this, I replaced the double permutation loop with two calls to Sort() and a single O(n) loop. D'oh! My solution to the small input was judged as correct, so I proceeded to the large input. It ran just as fast and so I submitted its output, too. Well, again, I screwed up with the requirements and it turns out my math was overflowing left, right and center and thus, when the contest ended, I got a measly 5 points (out of a possible 15 for that problem and out of a possible 100 for all 3 problems!), which means I ranked 2363 out of 2394. (you don't find out if your submission to the large version is correct until the end of the contest - I also only attempted the first problem)

Oh, well... I might try again in Round 1C (Sunday at 05:00 local time!), but in the meantime I thought I'd publish some of the source code that came out of this. I used Visual Studio 2008, which meant I could try out the neat features of the C# that came out with .NET 3.5, such as:
  • the var keyword
  • extension methods
  • LINQ

OK, so I didn't need to use LINQ, nor did I use extension methods until after the contest, but here's my touched-up Permutations iterator method, generalized to any IList<T> instance:
public static class Extension {
public static IEnumerable<IList<T>> Permutations<T> ( this IList<T> input ) {
int numElements = input.Count;
var slotOffsets = new int[numElements];
var slotBusy = new bool[numElements];
bool allDone = false;
while ( !allDone ) {
#region Set slotBusy flags to false
for ( int i = 0; i < numElements; i++ ) {
slotBusy[i] = false;
}
#endregion

IList<T> permutation = new List<T> ( numElements );
int lastSelected = -1;
for ( int i = 0; i < numElements; i++ ) {
for ( int j = 0; j < numElements; j++ ) {
int selectedSlot = ( lastSelected + 1 + j + slotOffsets[i] ) % numElements;
if ( !slotBusy[selectedSlot] ) {
slotBusy[selectedSlot] = true;
permutation.Add ( input[selectedSlot] );
lastSelected = selectedSlot;
break;
}
}
}
yield return permutation;

#region Update offsets
for ( int i = 0; i < numElements; i++ ) {
slotOffsets[i]++;
if ( slotOffsets[i] < ( numElements - i ) ) {
break;
}
else {
if ( i == numElements - 1 ) {
allDone = true;
}
else {
slotOffsets[i] = 0;
}
}
}
#endregion
}
}
}

...which you can use as follows (notice how it's magically a method on any IList<> implementation?):
IList<int> inputList = new List<int> ( new int[] { 1, 2, 3 } );
foreach ( List<int> permutation in inputList.Permutations ( ) ) {
StringBuilder sb = new StringBuilder ( );
sb.Append ( "[" );
bool isFirst = true;
foreach ( var item in permutation ) {
if ( !isFirst ) {
sb.Append ( ", " );
}
else {
isFirst = false;
}
sb.Append ( item );
}
sb.Append ( "]" );
Console.WriteLine ( sb.ToString() );
}

...and it should produce the following output:
[1, 2, 3]
[2, 3, 1]
[3, 1, 2]
[1, 3, 2]
[2, 1, 3]
[3, 2, 1]

I just wish I had prepared this method before the contest, although I think some actual practice in solving these kinds of problems would have helped me more. Maybe next time... :)

Tuesday, June 10, 2008

Announcing the web-screen-saver project

I finally got around to posting a little tidbit of source code I had lying around on my computer as the new open-source project web-screen-saver (really lame name, I know). Not only that, but the premise seems a bit lame, too. Who would want a screen-saver that runs in their web browser?

It turns out I do! I have been learning about aviation weather and the best way to stay fresh is to practice every day, so I took a really old Toshiba laptop (Pentium MMX @ 166 MHz, 16 MB RAM and a 2.1 GB HD), got it running again (which involved soldering frankensteining an equally-as-old power supply from a Futjitsu laptop, not to mention putting the disassembled laptop back together from memory) and set it on top of my refrigerator. This way, whenever I'm preparing meals, washing dishes, feeding the cat, etc. in the kitchen, I can take a minute or so to browse the latest weather.

Next up was figuring out how to get various semi-frequently-updated images to show up on the screen on a cycle. Various options popped up, such as:


  1. a cron job that would trigger wget on a few URLs with one of:

    1. a companion program that would convert the HTML pages to images and rescale pure images appropriately and a corresponding client (any of many file-system-based screen-savers)

    2. a static page that would cycle the pre-downloaded pages or images using a bit of JavaScript

    3. a number of static pages that serve up one pre-downloaded page or image for a little bit of time before redirecting to the next



  2. a static page that would cycle the live pages or images using a bit of JavaScript

  3. a number of static pages that serve up one live page or image for a little bit of time before redirecting to the next



Some of the above options don't make that much sense until you consider that aviation weather is the kind of information that's sensitive to updates (or the lack thereof) and so it is of the utmost importance to always have the latest version of, say, a TAF or a GFA cloud & weather map. With some web browsers reportedly overzealously caching pages or images coupled with sometimes unreliable internet connections, I was trying to explore the space of options so that I could potentially detect the worse case and do something about it.

I ended up deciding to see whether the current browsers that I use/care about indeed [still] suffered from that problem, since it would eliminate an entire class of problems/solutions. Turns out that improper browser caching no longer appears to be a problem, although this could also be due to proper configuration programming at the server end. That, and the prospect of having to write client-server code that scanned files coming from remote computers sounded more stupid every second I continued to think about it since, you know, web browsers do that sort of thing already!

A few evenings of light programming (i.e. tweaking a few lines of code here and there during a few boring moments of TV watching) and I've got what I think is "version 1.0"-worthy. It's running right now on the trusty laptop in Internet Explorer 6[1] in full-screen mode (hide the status bar, hit F11, then right-click on the toolbars to select auto-hide), although with this much RAM it just swaps and thrashes like hell for a minute or two on every reload. I also need to reboot it every so often because the USB wireless adapter's driver is as stable as the U.S. economy. *cough*

Download


Download version 1.0: awesome-web-screen-saver-1.0.html.
View source of version 1.0: index.html
Live demo (latest version): Aviation Weather Electronic Summary Of Major Events

How to use


Once everything loads, all but the first page/image will be "hidden" (it's really a clever lame container resizing trick). After 2 minutes, the next page/image will be shown, etc. until about 33 minutes at which point the whole thing will reload. You can, of course, scroll up and down, but you may want to use the left and right arrow keys, since they are programmed to jump back and forward (respectively) at page/image boundaries.

Roadmap for future versions



  • JavaScript code to live in its own file

  • META-based refresh to be adjusted based on the number of "pages"

  • better handling of unavailable content (i.e. keep retrying - with suitable back-off - iframes or images that returned anything other than HTTP 200)

  • use Google App Engine for the following features:

    • configurable content profiles (i.e. pick your own frequently-changing content)

    • content caching (i.e. Google's servers can withstand more hits and will probably replicate content to various globally-distributed data centers for higher availability)

    • content download scheduling (i.e. while you have both of the previous two features, you might as well configure content with an expected lifetime or scheduled release, so that it is only pulled once, per publishing, from its source)

    • stats collection

    • general geekiness





Notes


[1] This will probably cost me a few geek points, but the laptop is still running the Windows 98 copy it came with - I couldn't get any GNU/Linux distro to boot/install and I have better things to do with my time!

Thursday, May 08, 2008

"I must use this power only to annoy"

I clicked on an Amazon link to a product the other day and it lead me to its product page. Everything there is fine, except instead of a price, there was a link that read "Click here to see the price". There was also another link that read (Why don't we show the price?) which contained the following text:


Why Don't We Show the Price?

Manufacturers sometimes ask that retailers not display a price if it drops below a certain amount. The "click here to see price" message indicates that the price of the item is so low that the manufacturer requested that it not be advertised (that is, displayed). In a brick-and-mortar store, you would probably have to ask a salesperson what the price of the product is. At Amazon, by clicking on "click here to see price" you are essentially asking to see the price, at which point we show it to you.


Ummm... WTF? If I didn't see a price in a brick-and-mortar store, I wouldn't buy the item! You forcing me to click just to replicate that have-to-ask experience is forcing me to replicate my wouldn't-buy-the-item reaction. I hope you're happy about your lost sale.

Tuesday, April 29, 2008

Hi-tech alumni

What appeared to be a joke article at first glance (Cravath sounds like the french word for a tie) turned out to be a very insightful post by Alex on the topic of quitting and more generally about the habits of a certain class of software developers. I couldn't agree more! In fact, I have found myself proud to announce to my peers that I have worked at Microsoft and Macadamian; that I'm an alumnus or a graduate of sorts from their respective schools [of thought], that I embody some/most of their best practices and serve as a kind of unofficial ambassador.

I can relate to the "bus factor" concept and can even suggest a tangible metric: fantastic developers (who are thus likely to quit) are usually top contributors to corporate wikis, since they are constantly externalizing their knowledge for future colleagues/replacements. This, ironically, increases their value since they can spend more of their [precious] time solving new problems instead of being constantly disturbed for knowledge and acting as a "walking wiki" (which Alex calls "unskilled people"). It's not hard to see how true synergy is achieved by having all documentation at everybody's fingertips. This is what Bill Gates called a Digital Nervous System.

The next logical step to documenting is automating. So even if you feel your "bus factor" is low because everything is documented and your team spends a lot/most of their time solving new problems (which is, by itself, an excellent start) documented AND automated processes are the true mark of excellent talent.

So, in the spirit of the topic, I will quit my job (and there's nothing wrong with that), but I just haven't decided when. And if I start a company, I will include a link to this article in the corporate wiki.

P.S.: I'm posting this to my blog from Google Docs. I had this idea that it would be great to be able to do so and, lo and behold, there it was, under Share/Publish as web page...

Update: Ok, it posted, but without a title and the HTML was definitely not clean, but it's a start. I probably just need to tweak a few styles.

Friday, February 29, 2008

PalmPilot Jr

Having received my XO's developer key, I immediately upgraded the operating system to build number "Joyride 1638", using one of those "USB-based memory sticks". The large PDA/small laptop now appears to switch to some form of low-power mode whenever the lid is closed, when in "Reader mode" and in the middle of yum-based downloads. A tad too aggressive, if you ask me, but that's life when installing unstable software. It may have had something to do with having the Reader activity opened at the same time, but in any case, I don't think suspending is necessary when the laptop is plugged in. Maybe it would be OK after a much longer timeout, say 10 minutes.

Other than that, the new power management features (coming from build 653 -- which I think had absolutely none, except maybe for the "blank screen" screensaver -- these features are totally new) are really spiffy, and allowed me to go from a full charge this morning and still be going after a ride on the bus, followed by a full day of work (where it spent most of that time closed up). Hmmm... Maybe I didn't explain that correctly, so I'll switch to rough pseudo-code:


laptop.powerMode = PowerMode.OnBattery;
laptop.radio = RadioMode.Off;
me.use ( laptop, 30 /* minutes */ );
laptop.powerMode = PowerMode.Suspend;
Thread.sleep ( 1000 * 60 * 60 * 8 /* 8 hours in milliseconds */ );
laptop.powerMode = PowerMode.OnBattery;
me.use ( laptop, 30 /* minutes */ );
// because I didn't have to shutdown and boot up the laptop again
me.mood = Mood.Happy;


...there. I swear those "I write code" t-shirts were invented for me.


Anyway, there are still a few quirks, but the software is definitely getting there, which brings me to the whole point of this post: the XO should [eventually] be sold around the world. I'm thinking it could be ready and in all the stores where laptops and/or educational toys are normally sold (such as Walmart, Toys 'R' Us, ThinkGeek, Amazon.com, Future Shop, Best Buy, Fry's Electronics, Radio Shack, etc.) by next Christmas.

This form of distribution could expand their user base in a way that's not too dissimilar to the G1G1 program, minus the logistics nightmares associated with selling and shipping 80 000 units individually. This way, they ship say, 250 000 units in chunks of 10 or 20 thousand to a few retailers with established distribution channels and the foundation can then focus their efforts on what they do best (designing and building the laptop, marketing to governments of poor countries, etc.), instead of trying to also be a mass-distributor.

Of course, this assumes that the sale of XOs at retail would bring enough revenue to not only make this affordable to the foundation, but also to make money. I don't think they could pitch it for $400 (as in the G1G1 campaign) with a straight face (some Acer laptops are available now for $600), but they could probably do it for less than $200. At that price, they are starting to compete with other educational toys, as well as traditional PDAs that also come with a keyboard, word processing software, a web browser and an extended battery life.

I'm totally serious about this: it could be the source of volume they have been after to bring the price of the units down, not to mention the associated network effects. Think of it this way: as a parent, would you rather get your child an expensive, fragile computer made for adults or an inexpensive, rugged computer made for kids? ("hand-me-down clunkers" and sources of free tech support notwithstanding) How many people missed out due to the G1G1 campaign's short lifespan or high barrier of entry (i.e. you must create a PayPal account - even though the error message only said your credit card was not valid - and then have it authorized for large amounts - which means having it linked to a bank account - on top of the amateur-looking website and the uncertainty of shipping dates, etc.)?

Let's hope they read my blog and credit me after the associated success boom from implementing my master plan. You're welcome. :)