Thursday, October 28, 2010

Windows disk management with .NET framework

Windows has a very powerful programming API that allows developers almost total control over the way storage is managed. There are 3 main API subsets:
This article explores the undocumented API to VDS for managed code available in Windows Server 2008 and Windows Server 2008 R2.

Unfortunately, .NET framework does not ship with an official wrapper, so there are two ways to access VDS from managed code. VDS is exposed through COM, but has no automation support, so one could use .NET COM interop, but would have to declare the interface by hand. There is a good example of how to do it here: http://stackoverflow.com/questions/2755458/retrieving-virtual-disk-file-name-from-disk-number/2892042.

However, VDS is a relatively big API, and so doing the marshalling by hand is a challenging job. Of course, if you do this, the humanity will be eternally grateful (especially if you post the results to http://pinvoke.net/)!

For those of us who are lazy, the next best thing is to use the DLL that is present on Windows Server 2008 and above - Microsoft.Storage.Vds.dll. It lives in both GAC and SxS. In both cases the paths to the files are absolutely awful, and I will dare not utter them here. Instead, go to you Windows directory and do "dir /s Microsoft.Storage.Vds.dll". The resulting DLL you can then copy to your application and add a reference to it in your project.

Important note: the DLL is ONLY present in Server OS - not in consumer Windows - but the interop logic that it exposes will work on Windows 7 as well. You just have to carry the file around with your app (note of course that Microsoft does not grant you any rights to redistribute parts of Windows!) .

Another important note: to use VDS on a local machine, your application must have an admin token in its process. When debugging in Visual Studio, it helps to start VS "as Administrator". To ensure that the right privileges exist when running the application, add the manifest file as follows:
<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</asmv1:assembly>
The most important note: The managed API lacks one member of IVdsPack interface - the function that is absolutely crucial to creating RAID arrays (IVdsPack::MigrateDisks). As you can see in reflector, the IVdsPack is not completely marshalled ("slot7" occupies the place where MigrateDisks should have been):

This means that a new disk cannot be made dynamic using this interface, and therefore it cannot be used to build new disk arrays. If you need this functionality, you are screwed have to do COM interop by hand. You can still use this library with the reflector to extract much of the interop code. However, all the code needed to check the disk system health, and to replace a disk is present, which means that the library is very useful for many interesting scenarios. (Again, Microsoft does not give you a right to reverse-engineer parts of Windows to incorporate them into your application!)

With this in mind, on to managed VDS!

To use VDS, you have to first connect to VDS service. You can connect to a service both on your local machine as well as on a remote computer (providing that your user has admin rights on it:
using Microsoft.Storage.Vds;
...
ServiceLoader loader = new ServiceLoader();
Service vds = loader.LoadService("computername"); // or null
vds.WaitForServiceReady();
The object hierarchy in VDS is as follows. The VDS object contains a list of providers, and a list of unallocated disks. Unallocated disks are the disks that have not been initialized. You can access them as follows:
foreach (Disk d in vds.UnallocatedDisks)
{
    Console.WriteLine("Found unused disk {0} {1} {2} {3} {4}",
        d.FriendlyName, d.Name, d.DevicePath, d.Size, d.Health);
}
On most computers (excluding the very rare cases where hardware manufacturers exposed their custom storage systems through VDS, which can happen with SANs), there are two providers, a dynamic provider and basic provider. A dynamic provider contains dynamic disks - the disks that can contain RAID volumes (simple volumes, spanned volumes, RAID-0, RAID-1, and RAID-5). You make the disk dynamic in disk management applet:
 

The basic provider contains all the "normal", non-dynamic disks. To get dynamic and basic providers, do the following:
private static SoftwareProvider GetDynamicProvider(Service vds)
{
    foreach (Provider p in vds.Providers)
    {
        if ((p.Flags & ProviderFlags.Dynamic) != 0)
            return p as SoftwareProvider;
    }

    return null;
}


private static SoftwareProvider GetBasicProvider(Service vds)
{
    foreach (Provider p in vds.Providers)
    {
        if ((p.Flags & (ProviderFlags.Dynamic |
                ProviderFlags.InternalHardwareProvider)) == 0)
            return p as SoftwareProvider;
    }
    return null;
}
Provider contains a list of packs. There is usually only one pack in the dynamic provider, unless a set of dynamic disks initialized on a different computer was added, but not migrated (they appear as "foreign" disks in disk management applet). Basic disks are one per pack, so if there several basic disks in the system, there would be multiple packs in basic provider.

Here is how to print information on all the disks in a system:
SoftwareProvider dynamicProvider = GetDynamicProvider(vds);
foreach (Pack p in dynamicProvider.Packs)

{
    foreach (Disk d in p.Disks)
    {
        Console.WriteLine("Found dynamic disk {0} {1} {2} {3} {4}",
            d.FriendlyName, d.Name, d.DevicePath, d.Size, d.Health);
    }
}


SoftwareProvider basicProvider = GetBasicProvider(vds);
foreach (Pack p in basicProvider.Packs)
{
    foreach (Disk d in p.Disks)
    {
        Console.WriteLine("Found basic disk {0} {1} {2} {3} {4}",
            d.FriendlyName, d.Name, d.DevicePath, d.Size, d.Health);
    }
}


foreach (Disk d in vds.UnallocatedDisks)
{
    Console.WriteLine("Found unused disk {0} {1} {2} {3} {4}",
        d.FriendlyName, d.Name, d.DevicePath, d.Size, d.Health);
}


Among other things, Disk.Heath is a property that allows you to ferret out disks that are missing or unhealthy. From the reflector:
public enum Health

{
    Unknown = 0,
    Healthy = 1,
    Rebuilding = 2,
    Stale = 3,
    Failing = 4,
    FailingRedundancy = 5,
    FailedRedundancy = 6,
    FailedRedundancyFailing = 7,
    Failed = 8,
}
The pack also contains volumes, e.g.:
foreach (Volume v in p.Volumes)

{
    Console.WriteLine("Found volume {0}: {1} {2} {3} {4}",
        v.DriveLetter, v.Name, v.Type, v.Size, v.Health);
}
And, of course, given a disk, it is easy to figure out which volumes are on this disk (in case of RAID arrays, of course, the volume might not be on just one disk of course). A Disk structure has a list of extents in it. An extent has a volume id, which can be translated into a Volume object:
foreach (DiskExtent de in d.Extents)

{
    Volume v = vds.GetObject<Volume>(de.DiskId);

}
The reverse - finding out what disks a volume occupies - is only slightly more involved:
foreach (VolumePlex vp in v.Plexes)

{
    foreach (DiskExtent de in vp.Extents)
    {
        Disk d = vds.GetObject<Disk>(de.DiskId);
    }
}
Another useful piece of functionality in managed VDS interface is ability to replace a dead disk easily by using Pack.BeginReplaceDisk.

Time for a complete example. The following program scans a disk system on a running computer, finds disks that went missing/unhealthy, and sends an email to a pre-defined account.

Program.cs:
//-----------------------------------------------------------------------

// <copyright>
// Copyright (C) Sergey Solyanik.
//
// This file is subject to the terms and conditions of the Microsoft Public License (MS-PL).
// See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL for more details.
// </copyright>
//-----------------------------------------------------------------------


using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Net.Mail;
using System.Text;

using Microsoft.Storage.Vds;

namespace DiskMonitor
{
    /// <summary>
    /// Implements a simple disk monitor. The program is a periodic task
    /// that looks for an unhealthy disk and sends an email if it finds one.
    /// </summary>
    class Program
    {
        /// <summary>
        /// Main program.
        /// </summary>
        /// <param name="args"> Arguments from the command line. None expected. </param>
        static void Main(string[] args)
        {
            ServiceLoader loader = new ServiceLoader();
            Service vds = loader.LoadService(null);
            vds.WaitForServiceReady();


            StringBuilder sb = new StringBuilder();

            SoftwareProvider dynamicProvider = GetDynamicProvider(vds);
            foreach (Pack p in dynamicProvider.Packs)
            {
                foreach (Disk d in p.Disks)
                {
                    if (d.Health != Health.Healthy && d.Health != Health.Rebuilding)
                    {
                        sb.Append(string.Format("Disk {0} has health status {1}\n",
                            d.Name, d.Health));
                    }
                }
            }


            SoftwareProvider basicProvider = GetBasicProvider(vds);
            foreach (Pack p in basicProvider.Packs)
            {
                foreach (Disk d in p.Disks)
                {
                    if (d.Health != Health.Healthy && d.Health != Health.Rebuilding)
                    {
                        sb.Append(string.Format("Disk {0} has health status {1}\n",
                            d.Name, d.Health));
                    }

                }
            }

            if (sb.Length != 0)
            {
                Console.Error.Write(sb);
            }
            else
            {
                Console.WriteLine("Everything is great!");
            }

            MailMessage m = new MailMessage();
            m.To.Add(ConfigurationManager.AppSettings["NotificationEmail"]);
            m.From = new MailAddress(
           ConfigurationManager.AppSettings["FromEmail"]);
           m.Sender = new MailAddress(
           ConfigurationManager.AppSettings["FromEmail"]);
           m.IsBodyHtml = false;

            if (sb.Length != 0)
            {
                m.Subject = string.Format(
                    "Problems with disk array on {0}", Environment.MachineName);
                m.Body = sb.ToString();
            }
            else if (bool.Parse(ConfigurationManager.AppSettings["VerboseMail"]))
            {
                m.Subject = string.Format(
                    "Checked out disk system on {0}: everything is healthy!",
                    Environment.MachineName);
                m.Body = "Nothing to report.";
            }


            SmtpClient client = new SmtpClient(
            ConfigurationManager.AppSettings["SmtpServer"]);
            client.UseDefaultCredentials = true;
            client.EnableSsl = bool.Parse(
                ConfigurationManager.AppSettings["UseSslForSmtp"]);
            client.Send(m);
        }
        /// <summary>
        /// Finds the dynamic software provider.
        /// </summary>
        /// <param name="vds"> VDS service. </param>
        /// <returns> Basic software provider or null of none exists. </returns>
        private static SoftwareProvider GetDynamicProvider(Service vds)
        {
            foreach (Provider p in vds.Providers)
            {
                if ((p.Flags & ProviderFlags.Dynamic) != 0)
                    return p as SoftwareProvider;
            }
            return null;
        }

        /// <summary>
        /// Finds the basic software provider.
        /// </summary>
        /// <param name="vds"> VDS service. </param>
        /// <returns> Basic software provider or null of none exists. </returns>
        private static SoftwareProvider GetBasicProvider(Service vds)
        {
            foreach (Provider p in vds.Providers)
            {
                if ((p.Flags & (ProviderFlags.Dynamic |
                        ProviderFlags.InternalHardwareProvider)) == 0)
                    return p as SoftwareProvider;
            }
            return null;
        }
    }
}
app.config:
<?xml version="1.0" encoding="utf-8" ?>

<configuration>
  <appSettings>
    <add key="SmtpServer" value="smtp.server.domain.com" />
    <add key="NotificationEmail" value="target@domain.com" />
    <add key="FromEmail" value="user@domain.com" />
    <add key="UseSslForSmtp" value="true" />
    <add key="VerboseMail" value="true" />
  </appSettings>
</configuration>
app.manifest:
<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance>

  <assemblyIdentity version="1.0.0.0" name="DiskMonitor"/>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</asmv1:assembly>

 

Tuesday, October 19, 2010

How much is mobile entertainment market worth?

With AAPL approaching $300B, it is worth more than Toyota, Daimler, and Honda combined.

In fact, here is the list of top automobile manufacturers (which apparently includes ZAP, the electric scooter company):

Market Cap
Toyota Motor Corporation (ADR)112.13 Bil
Daimler AG (USA)70.73 Bil
HONDA MOTOR CO., LTD. (ADR)66.31 Bil
Volkswagen AG (ADR)53.73 Bil
Ford Motor Company47.46 Bil
Nissan Motor Co., Ltd. (ADR)38.77 Bil
AB Volvo (ADR)28.26 Bil
Fiat S.p.A. (ADR)20.35 Bil
Tata Motors Limited (ADR)12.22 Bil
Tesla Motors Inc1.91 Bil
Sorl Auto Parts, Inc.172.59 Mil
Azure Dynamics Corp161.25 Mil
ZAP56.02 Mil
Zenn Motor Co Inc55.25 Mil
Tongxin International Ltd37.68 Mil

Together, these companies are worth a combined total of $453.7B, only 50% more than AAPL. Wow. Just wow...

Thursday, September 30, 2010

David Patterson

David Patterson (of Computer Architecture: A Quantitative Approach) gave a lecture today at Microsoft about his ROC project.

Among other things, I learned two things:

...Peres's law

“If a problem has no solution, it may not be a problem, but a fact, not to be solved, but to be coped with over time”

    — Shimon Peres
 
...and that Bing is now a better search engine than Google.
 
For the last several months I was using Bing and Google more or less interchangeably, but Google was always an authoritative source. So when a few hours after the lecture I wanted to quote the Peres's law to a co-worker, I went to Google first. I typed in the following query: "unsolvable problems Shimon Peres facts cope". To my surprise, the quote was nowhere to be found:
 
 
I was really surprised, and tried Bing. Both of the top two results pointed to the quote.

Thursday, September 23, 2010

Taliban, v2

http://www.independent.co.uk/news/world/asia/how-the-cia-ran-a-secret-army-of-3000-assassins-2087039.html

Apparently CIA is financing an army in Afghanistan in exactly the same way they finances the anti-Soviet resistance which resulted in Osama Bin Ladin's movement. Smart people learn on other's mistakes. Dumb people learn on their own. And then there's a whole new level...

Saturday, September 11, 2010

Republican Lobbyist Edwina Rogers Wraps Gifts in Money



http://www.youtube.com/watch?v=xXj-oQm-NbE

I have no further comments... But there was one of note on Reddit: "The poor can dig the wrapper out of the trash later. This is called trickle down economics."

Court dismisses torture lawsuit because it "might expose state secrets"

"While the alleged abuses occurred during the Bush administration, the ruling added a chapter to the Obama administration’s aggressive national security policies.

Its counterterrorism programs have in some ways departed from the expectations of change fostered by President Obama’s campaign rhetoric, which was often sharply critical of former President George W. Bush’s approach.

Among other policies, the Obama national security team has also authorized the C.I.A. to try to kill a United States citizen suspected of terrorism ties, blocked efforts by detainees in Afghanistan to bring habeas corpus lawsuits challenging the basis for their imprisonment without trial, and continued the C.I.A.’s so-called extraordinary rendition program of prisoner transfers — though the administration has forbidden torture and says it seeks assurances from other countries that detainees will not be mistreated."
http://www.nytimes.com/2010/09/09/us/09secrets.html?_r=4&hp=&adxnnl=1&pagewanted=all&adxnnlx=1284220860-JSOfEHxr+sgBSLlDU9nJSA

It looks like I am voting for a third party candidate in the next election season...

Friday, September 3, 2010

Governor of Arizona



...and here's the explanation of this phenomenon from Reddit...

Monday, August 30, 2010

Israel: Your tax dollars at work

Looks like another terrorist is down.

    http://www.guardian.co.uk/world/2005/nov/16/israel2?CMP=twt_gu

From the watchtower "It's a little girl. She's running defensively eastward."

From the operations room "Are we talking about a girl under the age of 10?"

Watchtower "A girl about 10, she's behind the embankment, scared to death."

Watchtower "I think that one of the positions took her out."

Captain R "I and another soldier ... are going in a little nearer, forward, to confirm the kill ... Receive a situation report. We fired and killed her ... I also confirmed the kill. Over."

How to make a sniper rifle. Table of contents.

Part 1: Introduction
    http://www.preciseshooter.com/blog/MosinSniperPart1.aspx

Part 2: Buying a Mosin Nagant
    http://www.preciseshooter.com/blog/MosinSniperPart2.aspx

Part 3: Cleaning a Mosin Nagant
    http://www.preciseshooter.com/blog/MosinSniperPart3.aspx

Part 4: Disassembling a Mosin Nagant
    http://www.preciseshooter.com/blog/MosinSniperPart4.aspx

Part 5: Accurizing a Mosin Nagant
    http://www.preciseshooter.com/blog/MosinSniperPart5.aspx

Part 6: Selecting a scope mount
    http://www.preciseshooter.com/blog/MosinSniperPart6.aspx

Part 7: Mounting a side rail on the Mosin Nagant
    http://www.preciseshooter.com/blog/MosinSniperPart7.aspx

Thursday, July 22, 2010

Time sensitive material

I received this prospectus, dated 12/2009, yesterday.

Tuesday, July 20, 2010

World's most inefficient way to check for primeness (via Reddit)

http://www.noulakaz.net/weblog/2007/03/18/a-regular-expression-to-check-for-prime-numbers/

Fire your perl interpreter, feed this function 982451653, and watch your computer die a slow and painful death...

The only democracy in the Middle East (TM) (via Reddit)

A Palestinian man has been convicted of rape after having consensual sex with an Israeli woman who believed he was Jewish because he introduced himself as "Daniel".

http://www.telegraph.co.uk/news/worldnews/middleeast/israel/7901025/Palestinian-jailed-for-rape-after-claiming-to-be-Jewish.html

Sunday, July 18, 2010

Inception

Watched the highly acclaimed "Inception" on Friday. Below is my take.

Short version: "Titanic" is better.

And I don't say this because I like "Titanic".

My two favorite stories about "Titanic" are as follows. When the movie was first released on VHS, the MicroNews - internal MS paper that back then was a print edition - published this classified ad: "Titanic on VHS. First tape watched once. Second tape never watched".

Second story: among Windows developers Intel's Itanium chip is known as "Itanic". Although this one has nothing to do with the movie...

Long version: If you consider "Matrix" brainy and captivating, "Inception" is for you!

It is extremely similar to "Matrix". Both movies take a trivial idea ("Matrix": reality is actually a computer generated dream; "Inception": reality is influenced by a human-induced shared dreams) and make a movie out of it by adding large number of superficialities.

In "Matrix" it's tough looking impeccably clothed men beating main heroes up in highly choreographed fight scenes. I guess it must be the Hollywood's idea of what's going on inside a computer. I suppose if I had majored in communications, I would have thought about computers like that, too.

In "Inception" it is impeccably "architected" dreamscapes where the heroes get to confront - and conquer! - their demons. I guess it must be the Hollywood's idea of what's going on inside a human brain. I suppose if I had majored in said communications, I would have thought about the mechanics of human brain like that as well.

Why do I think "Titanic" is better? There, if one abstracts from the plot, one still gets to enjoy the vistas. In "Inception", unfortunately, there is no way to abstract from the plot...

How to make a service in .NET

Here's a complete, self-contained way to build a system service using .NET. I was looking for a way to do it on the internets, but most of the examples rely on .NET template (which relies on designer, which is ugly) and don't have a way to install the service programmatically.

Without much ado, here's the code. All of it. Just replace ServiceMainThread with your code, and you're done. It even supports installing multiple instances of itself.

//-----------------------------------------------------------------------
// <copyright>
// Copyright (C) Sergey Solyanik.
//
// This file is subject to the terms and conditions of the Microsoft Public License (MS-PL).
// See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL for more details.
// </copyright>
//----------------------------------------------------------------------- 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.ServiceProcess;
using System.Text;
using System.Threading;

namespace CSServiceHost
{
    /// <summary>
    /// Main service class.
    /// </summary>
    public class MyService : ServiceBase
    {
        /// <summary>
        /// The thread that contains the execution path for the service.
        /// </summary>
        Thread runner;

        /// <summary>
        /// Event which gets signalled when the service stops.
        /// </summary>
        EventWaitHandle stop;

        /// <summary>
        /// Processes the start event for service.
        /// </summary>
        /// <param name="args"></param>
        protected override void OnStart(string[] args)
        {
            stop = new EventWaitHandle(false, EventResetMode.ManualReset);
            runner = new Thread(ServiceMainThread);
            runner.Start();
        }

        /// <summary>
        /// Processes the stop event for service.
        /// </summary>
        protected override void OnStop()
        {
            stop.Set();
            runner.Join();
        }

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        /// <param name="args"> Program arguments. See help.</param>
        static void Main(string[] args)
        {
            if (args.Length > 0)
            {
                ProcessServiceCommand(args);
                return;
            }

            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[] 
            { 
                new MyService() 
            };

            ServiceBase.Run(ServicesToRun);
        }

        /// <summary>
        /// Executes service installation/uinstallation, or runs it as a process.
        /// </summary>
        /// <param name="args"> Program arguments.</param>
        private static void ProcessServiceCommand(string[] args)
        {
            string exe = Assembly.GetExecutingAssembly().Location;

            if ("/install".Equals(args[0], StringComparison.OrdinalIgnoreCase))
            {
                object[] assemblyAttributes =
                    Assembly.GetExecutingAssembly().GetCustomAttributes(false);

                string instance =
                    (from a in assemblyAttributes
                     where a is AssemblyTitleAttribute
                     select ((AssemblyTitleAttribute)a).Title).SingleOrDefault();
                string name =
                    (from a in assemblyAttributes
                     where a is AssemblyDescriptionAttribute
                     select ((AssemblyDescriptionAttribute)a).Description).SingleOrDefault();
                string account = null;
                string password = String.Empty;
                for (int i = 1; i < args.Length; ++i)
                {
                    if (args[i].StartsWith(
                        "/instance:", StringComparison.OrdinalIgnoreCase))
                    {
                        instance = args[i].Substring(10);
                    }
                    else if (args[i].StartsWith(
                        "/name:", StringComparison.OrdinalIgnoreCase))
                    {
                        name = args[i].Substring(6);
                    }
                    else if (args[i].StartsWith(
                        "/account:", StringComparison.OrdinalIgnoreCase))
                    {
                        account = args[i].Substring(9);
                    }
                    else if (args[i].StartsWith(
                        "/password:", StringComparison.OrdinalIgnoreCase))
                    {
                        password = args[i].Substring(10);
                    }
                    else
                    {
                        Console.Error.WriteLine("Could not parse: {0}", args[i]);
                    }
                }

                InstallService(exe, instance, name, account, password);
            }
            else if ("/uninstall".Equals(
                args[0], StringComparison.OrdinalIgnoreCase))
            {
                object[] assemblyAttributes =
                    Assembly.GetExecutingAssembly().GetCustomAttributes(false);
                string instance =
                    (from a in assemblyAttributes
                     where a is AssemblyTitleAttribute
                     select ((AssemblyTitleAttribute)a).Title).SingleOrDefault();

                for (int i = 1; i < args.Length; ++i)
                {
                    if (args[i].StartsWith(
                        "/instance:", StringComparison.OrdinalIgnoreCase))
                    {
                        instance = args[i].Substring(10);
                    }
                    else
                    {
                        Console.Error.WriteLine("Could not parse: {0}", args[i]);
                    }
                }

                UninstallService(instance);
            }
            else if ("/run".Equals(args[0], StringComparison.OrdinalIgnoreCase))
            {
                MyService service = new MyService();
                service.OnStart(new string[0]);
                Console.WriteLine("Service is running as a process.");
                Console.WriteLine("Press <ENTER> to stop and exit.");
                Console.ReadLine();
                service.OnStop();
            }
            else
            {
                Console.WriteLine("To install service:");
                Console.WriteLine("    {0} /install", exe);
                Console.WriteLine("        [/instance:instance_name [/name:display_name]]");
                Console.WriteLine("        [/account:account [/password:password]]");
                Console.WriteLine("To uninstall service:");
                Console.WriteLine("    {0} /uninstall [/instance:instance_name]", exe);
                Console.WriteLine("To run as a regular process:");
                Console.WriteLine("    {0} /run", exe);
            }
        }

        /// <summary>
        /// Installs service.
        /// </summary>
        /// <param name="exe"> Path to the executable. </param>
        /// <param name="instance"> Name of the service instance. </param>
        /// <param name="name"> Display name of the service. </param>
        /// <param name="account"> Account name or NULL for LocalSystem. </param>
        /// <param name="password"> Password or empty string if any of
        /// the machine accounts. </param>
        private static void InstallService(
            string exe,
            string instance,
            string name,
            string account,
            string password)
        {
            IntPtr scm = Win32.OpenSCManager(
                null, null, Win32.SC_MANAGER_CREATE_SERVICE);
            if (scm.ToInt32() == 0)
            {
                Console.Error.WriteLine(
                    "Failed to open SCM (error {0}).", Win32.GetLastError());
                return;
            }

            try
            {
                IntPtr service = Win32.CreateService(
                    scm,
                    instance,
                    name,
                    Win32.SERVICE_ALL_ACCESS,
                    Win32.SERVICE_WIN32_OWN_PROCESS,
                    Win32.SERVICE_AUTO_START,
                    Win32.SERVICE_ERROR_NORMAL,
                    exe,
                    null,
                    0,
                    null,
                    account,
                    password);

                if (service.ToInt32() == 0)
                {
                    Console.Error.WriteLine(
                        "Failed to create service (error {0}).",
                        Win32.GetLastError());
                    return;
                }

                try
                {
                    if (Win32.StartService(service, 0, null) == 0)
                    {
                        Console.Error.WriteLine(
                            "Failed to start service (error {0}).",
                            Win32.GetLastError());
                    }
                    else
                    {
                        Console.WriteLine(
                            "Service installed successfully.");
                    }
                }
                finally
                {
                    Win32.CloseServiceHandle(service);
                }
            }
            finally
            {
                Win32.CloseServiceHandle(scm);
            }
        }

        /// <summary>
        /// Uninstalls service.
        /// </summary>
        /// <param name="instance"> Service instance. </param>
        private static void UninstallService(string instance)
        {
            IntPtr scm = Win32.OpenSCManager(
                null, null, Win32.SC_MANAGER_ALL_ACCESS);
            if(scm.ToInt32() == 0)
            {
                Console.Error.WriteLine(
                    "Failed to open SCM (error {0}).",
                    Win32.GetLastError());
                return;
            }

            try
            {
                IntPtr service = Win32.OpenService(
                    scm, instance, Win32.DELETE | Win32.SERVICE_STOP);
                if (service.ToInt32() == 0)
                {
                    Console.Error.WriteLine(
                        "Failed to open service (error {0}).",
                        Win32.GetLastError());
                    return;
                }

                try
                {
                    Win32.SERVICE_STATUS stat;
                    if (0 == Win32.ControlService(
                        service, Win32.SERVICE_CONTROL_STOP, out stat))
                    {
                        Console.Error.WriteLine(
                            "Could not stop the service (error {0}).",
                            Win32.GetLastError());
                    }

                    while (Win32.QueryServiceStatus(service, out stat) != 0
                        && stat.dwCurrentState != Win32.SERVICE_STOPPED)
                    {
                        Thread.Sleep(1000);
                    }

                    if (Win32.DeleteService(service) == 0)
                    {
                        Console.Error.WriteLine(
                            "Failed to delete service (error {0}).",
                            Win32.GetLastError());
                    }
                    else
                    {
                        Console.WriteLine(
                            "Service successfully uninstalled.");
                    }
                }
                finally
                {
                    Win32.CloseServiceHandle(service);
                }
            }
            finally
            {
                Win32.CloseServiceHandle(scm);
            }
        }

        /// <summary>
        /// The actual logic.
        /// </summary>
        private void ServiceMainThread()
        {
            for (; ; )
            {
                if (stop.WaitOne(10000))
                {
                    break;
                }

                using (StreamWriter w =
                    new StreamWriter(@"c:\temp\testservice.txt", true))
                    w.WriteLine(
                        "Tick {0} {1}",
                        DateTime.Now,
                        Environment.UserName);
            }
        }

        /// <summary>
        /// Win32 thunks.
        /// </summary>
        private static class Win32
        {
            public const UInt32 SC_MANAGER_ALL_ACCESS = 0xF003F;
            public const UInt32 SC_MANAGER_CREATE_SERVICE = 0x0002;

            public const UInt32 SERVICE_WIN32_OWN_PROCESS = 0x00000010;
            public const UInt32 SERVICE_AUTO_START = 0x00000002;
            public const UInt32 SERVICE_ERROR_NORMAL = 0x00000001;

            public const UInt32 STANDARD_RIGHTS_REQUIRED = 0xF0000;
            public const UInt32 SERVICE_QUERY_CONFIG = 0x0001;
            public const UInt32 SERVICE_CHANGE_CONFIG = 0x0002;
            public const UInt32 SERVICE_QUERY_STATUS = 0x0004;
            public const UInt32 SERVICE_ENUMERATE_DEPENDENTS = 0x0008;
            public const UInt32 SERVICE_START = 0x0010;
            public const UInt32 SERVICE_STOP = 0x0020;
            public const UInt32 SERVICE_PAUSE_CONTINUE = 0x0040;
            public const UInt32 SERVICE_INTERROGATE = 0x0080;
            public const UInt32 SERVICE_USER_DEFINED_CONTROL = 0x0100;
            public const UInt32 SERVICE_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED |
                SERVICE_QUERY_CONFIG | SERVICE_CHANGE_CONFIG |
                SERVICE_QUERY_STATUS | SERVICE_ENUMERATE_DEPENDENTS |
                SERVICE_START | SERVICE_STOP | SERVICE_PAUSE_CONTINUE |
                SERVICE_INTERROGATE | SERVICE_USER_DEFINED_CONTROL;

            public const UInt32 DELETE = 0x10000;

            public const UInt32 SERVICE_CONTROL_STOP = 0x00000001;
            public const UInt32 SERVICE_STOPPED = 0x00000001;

            [StructLayout(LayoutKind.Sequential)]
            public struct SERVICE_STATUS
            {
                public UInt32 dwServiceType;
                public UInt32 dwCurrentState;
                public UInt32 dwControlAccepted;
                public UInt32 dwWin32ExitCode;
                public UInt32 dwServiceSpecificExitCode;
                public UInt32 dwCheckPoint;
                public UInt32 dwWaitHint;
            };

            [DllImport("advapi32.dll")]
            public static extern IntPtr OpenSCManager(
                string lpMachineName,
                string lpSCDB,
                UInt32 scParameter);
            
            [DllImport("advapi32.dll")]
            public static extern IntPtr CreateService(
                IntPtr SC_HANDLE,
                string lpSvcName,
                string lpDisplayName,
                UInt32 dwDesiredAccess,
                UInt32 dwServiceType,
                UInt32 dwStartType,
                UInt32 dwErrorControl,
                string lpPathName,
                string lpLoadOrderGroup,
                int lpdwTagId,
                string lpDependencies,
                string lpServiceStartName,
                string lpPassword);

            [DllImport("advapi32.dll")]
            public static extern void CloseServiceHandle(IntPtr SCHANDLE);

            [DllImport("advapi32.dll")]
            public static extern int StartService(
                IntPtr SVHANDLE,
                UInt32 dwNumServiceArgs,
                string lpServiceArgVectors);

            [DllImport("advapi32.dll", SetLastError = true)]
            public static extern IntPtr OpenService(
                IntPtr SCHANDLE,
                string lpSvcName,
                UInt32 dwNumServiceArgs);

            [DllImport("advapi32.dll", SetLastError = true)]
            public static extern int ControlService(
                IntPtr SCHANDLE,
                UInt32 dwControl,
                [MarshalAs(UnmanagedType.Struct)]
                out SERVICE_STATUS lpServiceStatus);

            [DllImport("advapi32.dll", SetLastError = true)]
            public static extern int QueryServiceStatus(
                IntPtr SCHANDLE,
                [MarshalAs(UnmanagedType.Struct)]
                out SERVICE_STATUS lpServiceStatus);

            [DllImport("advapi32.dll")]
            public static extern int DeleteService(IntPtr SVHANDLE);

            [DllImport("kernel32.dll")]
            public static extern int GetLastError();
        }
    }
}

Thursday, July 8, 2010

Microsoft Mini

Microsoft Mini blog has a lot of angst today with new round of layoffs at Microsoft.

http://minimsft.blogspot.com/2010/07/kin-fusing-kin-clusion-to-kin-and-fy11.html

I tried to comment, but the Blogger was broken and wouldn't accept it. Since I already typed it up, I am posting it here instead...

I worked at Microsoft for 10 years, then went to Google, then back to Microsoft.

All companies have problems. Apple. Google. Microsoft. They are just different problems, and they look bigger when you are closer to them.

Yes, Microsoft is failing in consumer markets, always have, maybe always will. We don't get consumer. Vista (AKA Abby & Toby platform), Windows Mobile, Kin... Even XBox - it is successful because it was built more for a typical Microsoft employee than for a regular person, it's just MS people love the same kind of videogames that 14 years old males do :-). But if you look at kids or family games on XBox - total failure.

The problem is that we target some mythical "dumb" customer, we don't really know who that is, and we overshoot the level of dumbness by a wide margin. I worked on the first version of Windows Home Server and we had people on the team - not developers, obviously - who seriously tried to argue that our customers don't know what a file share is. I kid you not.

However, just like Microsoft doesn't get consumers, Google and Apple don't get the enterprise. I have participated in creation of a business product at Google, and the people around me did not understand basic concepts like the need for customer service, a refund process, or the like. The entire Google internal system is antithetic to schedule predictability and release stability that is required for a corporate product. People who say that Gmail and Google Docs somehow threaten Exchange and Office have obviously never used these products in a work setting for an extended period of time.

One thing that is going for Microsoft is the plentitude of cultures. We have Xbox team, and an Office team, and Windows team, and Bing, and all these organizations are as unlike each others as they can be. My advice to people, especially developers, who complain about politics, poor managers, boring products, etc - check out the career site! Plenty of teams are hiring, and there are tons of really fun places which will match your preferred style, values, or culture. Just keep moving until you find the right place for YOU. Trust me, it does exist. (By the way, I am hiring, too! If you dream in code and can implement a semaphore if I woke you up at 3am, and like a blend of "old Microsoft" and "new Google" cultures, look me up on career site!)

Finally, it is true that often leaders make companies/armies/countries great. But not always, and never alone, and certainly not in democratic societies :-). I don't think that success of Microsoft in the 90's is directly attributable to BillG and BillG alone, and the steam somehow magically went out of the company the day he left the building. Yes, we have plenty of people at the high places that probably should not have been there. So does Google, so does Apple, so does Oracle, Intel, ..., ..., ... These aren't the people who (most of) you work with, they aren't the people who you meet every day, and I'll let you in on a secret - they aren't the people who make YOUR product a success or a failure. YOU do. They can't affect the stock price much - YOU can, by shipping great products, and by making environment around YOU better, so it attracts more people like YOU.

So don't get consumed by paranoia and politics, focus on your job and your team, and everything else will follow.

And if not, as long as you do the above, you will still be very employable. At Google, at Amazon, or in my team :-).

Sunday, July 4, 2010

Republican party, circa 1956

Republican Party Platform of 1956

http://www.presidency.ucsb.edu/ws/print.php?pid=25838

This is what DemocraticUnderground.com had to say about it:

"By these standards, modern Democrats have become Republicans, and modern Republicans have become batshit crazy. I think I’ve heard Kucinich mention Taft-Hartley, but you get a blank stare if you mention it to anybody under the age of 40. And to think that it used to be a topic of polite conversation among the political classes only 54 years ago."

This: http://journals.democraticunderground.com/eridani/449 has TL;DR.

Incidentally, I am hearing that the health care reform passed by Obama is very, very similar to the Republican counter-offer to the Clinton health plan.

Saturday, June 26, 2010

Friday, June 4, 2010

Terrorism...

Thirty years ago Chief of Staff Mordechai Gur observed that since 1948, "we have been fighting against a population that lives in villages and cities." As Israel's most prominent military analyst, Zeev Schiff, summarized his remarks, "the Israeli Army has always struck civilian populations, purposely and consciously...the Army, he said, has never distinguished civilian [from military] targets...[but] purposely attacked civilian targets."

http://www.chomsky.info/articles/20090119.htm

Wednesday, May 19, 2010

Can we stuff 2000 calories in one drink? Yes, we can! (via Reddit)

This is really, truly horrible: http://worldmysteries9.blogspot.com/2010/05/harmful-drinks-in-america.html

The worst-worst drink has 2,010 calories, 131 g fat (68 g saturated), 153 g sugars (this is the number of calories a human should consume in a day; and more fat than one probably should have in a week).

#4 on the list has 1,210 calories, 19 g fat (10 g saturated), and 240 g sugars! Yes, this is really 1/4 of a kg. In one "drink"!!!

Monday, May 17, 2010

Server 200x RAID tips and tricks

Since 2003, Windows Server was shipping with a very cool feature: software RAID.

Software RAID has two major advantages over hardware.

First, hardware RAID protects you against the disk failure, but not against the controller failure. The drive array contains proprietary disk allocation information that varies from manufacturer to manufacturer and controller to controller, so disks are not easily movable between different controllers. So when your controller fails - potentially, long time in the future, when the same boards are no longer available, - or a new release of OS stops supporting your older driver, you may be very much out of luck with your data.

Second, software RAID is considerably more flexible than hardware RAID. Hardware RAID operates on disks as atomic units - you RAID the whole disks together. Software RAID operates on volumes, and each volule can be configured with different level of redundancy. For example, you can have 2 constructs on two disks at the same time - an OS partition that is RAID-1 mirrored with an image on the second disk, and another volume that combines the rest of the space on two drives as a single span or RAID-0. The level of redundancy is selected per volume, not per disk.

Also, for those of us who like coding, software RAID has a very nice software interface (http://msdn.microsoft.com/en-us/library/bb986750(v=VS.85).aspx and its undocumented managed counterpart in Microsoft.Storage.Vds.dll) which allows one to code simple things like checking the health of the storage and send an email if something goes bad.

But what about performance? A while ago when we were designing Windows Home Server, we tested various hardware RAID implementations versus software RAID in Server 2003.

It turns out that both RAID-0 and RAID-1 exhibit very similar performance for both hardware and software solutions. If you think about what the system has to do (write the same data to two disks at the same time in the case of RAID-1) it quickly becomes obvious that hardware implementation does not really add anything over the software in this case: both can write the same data in two places at the same time with the same speed. Big surprise :-).

RAID-5 is a different beast though - there actually is a computation going on, and it is possible to build a specialized chip for doing vector XOR operations that would leave the general purpose x86 in the dust.

A much bigger problem also exists in the lack of integration between the formatting and the RAID code. When you format a RAID drive, the default allocation unit that the UI presents is very small.

Due to the way the software RAID is implemented, it leads to incredibly slow performance. On my relatively powerful system the writes clock at only 20-30MBps (this going to the drives that are supposed to sustain 3Gbps, or 300MBps transfers). Selecting a more reasonable allocation unit of 64k improves the write speed by a factor of four, to almost 120MBps.

The other performance problem that is format related is after creating a new RAID volume, the default behavior is that format and resync happen at the same time. I covered it in the previous blog post here: http://1-800-magic.blogspot.com/2010/05/solution-to-slow-formatting-puzzle.html.

In summary, here are the two very simple rules can make your RAID array much faster:
- Select the 64k as a default allocation unit when formatting the RAID-5 volume.
- When formatting any new RAID volume, use quick format first, wait for the volume to finish resyncing, then repeat with full format if you like (remember to keep the large allocation unit in the second format though!)

Happy RAIDing!

Solution to the slow formatting puzzle

A couple of weeks back I posted a puzzle about an experience that I just had with Server 2008 R2 software RAID subsystem: somehow the speed of formatting a new very large RAID-5 array was highly unpredictable. It advanced by a mere 12% a day for the 4 days and then suddenly sped up 4x and finished the last 50% of the formatting in just one day.

Furthermore, I expected precisely this behavior. Why?

This weirdness occurs because of the typical Microsoft phenomenon that was best expressed by an acquaintance of mine that works in Windows Mobile. He thought that the biggest difference between Microsoft's and Apple's approaches to development are these:

Microsoft attacks problems horizontally: it builds core system, then builds layers on top of it. When it's time to ship and something needs to get cut, it's the top of the stack that goes first - and more often than not it's the user experience.

Apple develops software vertically - it enables a user experience, from top to bottom. When they need to cut, they cut the entire experiences (and also maybe the bugs, judging from the fact that my iPhone seems to crash considerably more often than most of Windows Mobile phones I owned in the past). So for example, iPhone would ship with Bluetooth support just for the mono phone headset, but no stereo profile. But the experiences that are left are implemented completely, to the maximum possible level of usability.

Back to our RAID problem. The reason the formatting is so slow in the very beginning is because two things happen at the same time: RAID resync and the formatting. Resync gets kicked off immediately after the RAID system is built, and it simply ensures that the parity volume (in the case of RAID-5) has correct checksums, or the mirrored volume (in the case of RAID-1) has a correct copy of the primary volume.

The other thing that happens once you create RAID volume from the UI is, of course, formatting. One can select quick format which only creates a file system. However, most people probably prefer to run full format when a new drive is added, just to make sure that it is not full of bad sectors.

So we have two write operations that are going on - first one is creating the redundant information, the other is filling the disk with zeroes.

Writing to the disk is very fast these days, but the seek time has barely improved in the last decade, and because the two writes happen in different places on the disk, the whole thing is completely dominated by the disk head moving from one track to another. And with a seek time of 15ms you can only have ~70 of these per second.

Of course, if the format is in progress, there is absolutely no sense to do a resync at the same time - whatever redundancy data resync creates is going to be instantly overwritten by the format.

But there was no Steve Jobs standing over the devs' shoulder, and getting the (filesystem) format in sync with (block device) RAID required two different teams to do something together so it probably got punted. I am sure it's in in a readme somewhere... and now in at least one blog :-).

So to avoid unnecesary delays, select quick format option when adding the RAID volume, wait for the resync to finish, THEN format it again with the full format.

Saturday, May 1, 2010

Formatting the large hard drive - continued

As mentioned in the previous post, one of my servers is formatting a 7.5TB RAID-5 volume.

During the first 24 hours, it has completed 13%.
During the next 24 hours, the progress was at 26%.
Next day, it was at 38%, and the day after (last night) it clocked at 50%.
However, this morning, 12 hours later, it was at 75%, and tonight I expect it to be completed.

So in the very last day the system's progress was the same as for the first 4 days. Moreover, I fully expected this to be the case. Can anyone guess why?

Wednesday, April 28, 2010

Formatting a large RAID-5 drive

Old hard drives - I have had them for 3 years - on my servers at home started to fail, so I had to replace one RAID-5 array. I bought 5 2TB drives and used software RAID-5 function in Windows Server 2008 R2 to create a monstrous 7.5TB (usable space) drive out of these 5 disks.

I only use soft RAID because the disks can be read by any Windows Server, whereas hardware RAID protects well against a drive failure, but if it is the controller that dies, you may be out of luck - who knows if this model would be even available a few years down the road, and most controllers use proprietary data formats, so the disk arrays are not portable between them.

Anyway, I built the array and starting formatting it. After the first 24 hours, the format was 13% complete. Next day (today) it is up to 26%.

Puzzle for the readers - how long do you think the format is going to take in total? Do not post the reasoning, just the number :-). I will post the final answer (and why) when the format completes.

Wednesday, April 21, 2010

Dictionary attacks on my home network

I am running Small Business Server 2003 at home. One service that it provides is sending daily reports about what happened to the system in the last 24 hours.

This report includes summaries of the event logs, and in the last several days the security logs were overflowing with logon audit failures caused by what looks like a distributed dictionary attack on my server.

The attack goes as follows: for a few hours someone (a script, really) is trying to connect to the system using various "likely" account names like aloha, admin, Administrator, master, root, randy, etc. The logon attempt for every user name repeats a few dozen times, most likely with different passwords, although security logs obviously don't show them, and then the user name changes.

After a few thousand attempts the attack subsides only to resume the next day from a different IP address.

Obviously, all the passwords used by our family are rather complex, and it is very unlikely that the thing will ever guess them, but it's discomforting nevertheless, so today I decided to get rid of the attackers altogether.

Small Business Server 2003 does not include standard Windows Firewall, instead it uses filters in its routing service to block unwelcome traffic.

The UI for this "firewall" of sorts can be accessed via Routing and Remote Access MMC available in the Administrative Tools menu. From there, one can expand IP Routing, the NAT/Basic Firewall, and double-clicking on the interface name brings up a properties dialog box with the "Inbound Filters" button.

I had no idea how to access it programmatically. Luckily, most of Windows networking management is scriptable through netsh, and netsh has a very nice property called dump which prints out a script that could be used to reproduce the current configuration.

So I created a rule with the fake firewall rule with an address that I could recognize, ran netsh dump, and searched the output for that ip address. As it turns out then, to add a firewall rule on Server 2003 with routing enabled, this is what needs to be done:
routing ip add filter name="Network Connection"
    filtertype=INPUT srcaddr=xx.xx.xx.xx srcmask=255.255.255.255
    dstaddr=0.0.0.0 dstmask=0.0.0.0 proto=ANY
where xx.xx.xx.xx is the placeholder for the ip address.

Poking around a bit more, it turned out that to list the existing rules (thus determining which IP addresses have already been blocked), one could do this:
routing ip show filter name="Network Connection"
The output is a table which can easily be picked apart with a regular expression:
private static Regex presentIps = new Regex(
    @"^\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+255.255.255.255\s+.*$",
    RegexOptions.Multiline);
private static HashSet<string> GetCurrentlyBlockedIps()
{
    string filters = NetSh(
        "routing ip show filter name=\"Network Connection\"");
    MatchCollection allMatches = presentIps.Matches(filters);
    HashSet<string> blockedIps = new HashSet<string>();
    foreach (Match m in allMatches)
    {
        Console.WriteLine("This ip is currently being blocked: {0}",
            m.Groups[1].Value);
        blockedIps.Add(m.Groups[1].Value);
    }
    return blockedIps;
}
NetSh() is the function that runs netsh.exe. It's a bit complex because it reads both STDOUT and STDERR. This has to be done asynchronously or else a read operation on STDOUT might block while STDERR overflows:
private delegate string ReaderDelegate();
private static string NetSh(string arguments)
{
    Process process = new Process();
    process.StartInfo.Arguments = arguments;
    process.StartInfo.CreateNoWindow = true;
    process.StartInfo.FileName = "netsh.exe";
    process.StartInfo.RedirectStandardError = true;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.UseShellExecute = false;

    process.Start();

    ReaderDelegate stdoutReader =
        new ReaderDelegate(process.StandardOutput.ReadToEnd);
    ReaderDelegate stderrReader =
        new ReaderDelegate(process.StandardError.ReadToEnd);
    IAsyncResult stdoutResult = stdoutReader.BeginInvoke(null, null);
    IAsyncResult stderrResult = stderrReader.BeginInvoke(null, null);

    WaitHandle[] handles =
    {
        stdoutResult.AsyncWaitHandle,
        stderrResult.AsyncWaitHandle
    };

    if (!WaitHandle.WaitAll(handles))
        throw new Exception("netsh.exe was aborted");

    string stdout = stdoutReader.EndInvoke(stdoutResult);
    string stderr = stderrReader.EndInvoke(stderrResult);

    process.WaitForExit();

    if (!string.IsNullOrEmpty(stderr))
    {
        Console.Error.WriteLine(
            "Failed netsh {0}", process.StartInfo.Arguments);

        if (stdout != null)
            Console.Error.WriteLine("{0}", stdout);

        Console.Error.WriteLine("{0}", stderr);

        throw new Exception("netsh.exe failed");
    }

    process.Dispose();

    return stdout;
}

We're almost there. All I now need to do is to walk the security event log, picking out the IP address of the attacker out of the relevant entries:
using (EventLog ev = new EventLog("Security"))
{
    EventLogEntryCollection entries = ev.Entries;
    for (int index = entries.Count - 1; index >= 0; index--)
    {
        EventLogEntry ele = entries[index];
...
        if (ele.CategoryNumber != 2 || ele.InstanceId != 529 ||
            ele.EntryType != EventLogEntryType.FailureAudit)
            continue;

        if (ele.ReplacementStrings.Length < 11 ||
            knownGoodIps.IsMatch(ele.ReplacementStrings[11]))
            continue;

        string ip = ele.ReplacementStrings[11];
...
If the logon failure happens, say, more then 5 times in two minutes, we simply block out the whole ip address from accessing the server, as follows (you'll notice that I maintain a queue of events for all relevant ip addresses that allows me to detect the number of failures per time interval):
if (blockedIps.Contains(ip))
    continue;

LinkedList<DateTime> queue;
if (!trackedIps.ContainsKey(ip))
{
    queue = new LinkedList<DateTime>();
    queue.AddFirst(ele.TimeGenerated);
    trackedIps[ip] = queue;

    continue;
}

queue = trackedIps[ip];
queue.AddFirst(ele.TimeGenerated);

if (queue.Count < failEvents)
    continue;

while (queue.Count > failEvents)
    queue.RemoveLast();

TimeSpan period = queue.Last.Value - queue.First.Value;
if (period.Milliseconds > failPeriod)
    continue;

blockedIps.Add(ip);

NetSh("routing ip add filter name=\"Network Connection\" filtertype=INPUT srcaddr="
    + ip + " srcmask=255.255.255.255 dstaddr=0.0.0.0 dstmask=0.0.0.0 proto=ANY");

The rest is just accounting. Here's the full program for your enjoyment:
//-----------------------------------------------------------------------
// <copyright>
// Copyright (C) Sergey Solyanik.
//
// This file is subject to the terms and conditions of the Microsoft Public License (MS-PL).
// See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL for more details.
// </copyright>
//----------------------------------------------------------------------- 
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Net.Mail;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;

namespace Defender
{
    /// <summary>
    /// This implements code that scans security event log and uses Windows Server 2003
    /// ip routing filters to block dictionary attacks.
    /// </summary>
    class Program
    {
        /// <summary>
        /// Regular expression that parses existing filters out of
        /// netsh ip show filter output.
        /// </summary>
        private static Regex presentIps = new Regex(
            @"^\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+255.255.255.255\s+.*$",
            RegexOptions.Multiline);

        /// <summary>
        /// Delegate used to read process output.
        /// </summary>
        /// <returns> STDOUT/ERR stream converted to a string. </returns>
        private delegate string ReaderDelegate();

        /// <summary>
        /// Runs netsh.exe with the given argument. Throws if there is an error, else
        /// returns the output as one string.
        /// </summary>
        /// <param name="arguments"> netsh.exe command arguments. </param>
        /// <returns> STDOUT converted to one string. </returns>
        private static string NetSh(string arguments)
        {
            Process process = new Process();
            process.StartInfo.Arguments = arguments;
            process.StartInfo.CreateNoWindow = true;
            process.StartInfo.FileName = "netsh.exe";
            process.StartInfo.RedirectStandardError = true;
            process.StartInfo.RedirectStandardOutput = true;
            process.StartInfo.UseShellExecute = false;

            process.Start();

            ReaderDelegate stdoutReader =
                new ReaderDelegate(process.StandardOutput.ReadToEnd);
            ReaderDelegate stderrReader =
                new ReaderDelegate(process.StandardError.ReadToEnd);
            IAsyncResult stdoutResult = stdoutReader.BeginInvoke(null, null);
            IAsyncResult stderrResult = stderrReader.BeginInvoke(null, null);

            WaitHandle[] handles =
            {
                stdoutResult.AsyncWaitHandle,
                stderrResult.AsyncWaitHandle
            };

            if (!WaitHandle.WaitAll(handles))
            {
                throw new Exception("netsh.exe was aborted");
            }

            string stdout = stdoutReader.EndInvoke(stdoutResult);
            string stderr = stderrReader.EndInvoke(stderrResult);

            process.WaitForExit();

            if (!string.IsNullOrEmpty(stderr))
            {
                Console.Error.WriteLine(
                    "Failed netsh {0}", process.StartInfo.Arguments);

                if (stdout != null)
                    Console.Error.WriteLine("{0}", stdout);

                Console.Error.WriteLine("{0}", stderr);

                throw new Exception("netsh.exe failed");
            }

            process.Dispose();

            return stdout;
        }

        /// <summary>
        /// Gets the collection of ips that are already blocked.
        /// </summary>
        /// <returns> HashSet of blocked ips. </returns>
        private static HashSet<string> GetCurrentlyBlockedIps()
        {
            string filters = NetSh(
                "routing ip show filter name=\"Network Connection\"");
            MatchCollection allMatches = presentIps.Matches(filters);
            HashSet<string> blockedIps = new HashSet<string>();
            foreach (Match m in allMatches)
            {
                Console.WriteLine("This ip is currently being blocked: {0}",
                    m.Groups[1].Value);
                blockedIps.Add(m.Groups[1].Value);
            }

            return blockedIps;
        }

        /// <summary>
        /// Convers a comma-separated list of "known good" ips into a filtering
        /// regular expression.
        /// </summary>
        /// <param name="ipList">Comma-separated list of known-good ips.</param>
        /// <returns>Regular expression that matches this list.</returns>
        private static Regex BuildRegexForIpList(string ipList)
        {
            return new Regex(
                "^(" + ipList.Replace(".", "\\.").Replace(',', '|') + ").*$");
        }

        /// <summary>
        /// Sends notification email.
        /// </summary>
        /// <param name="mailBody"> Body of the email. </param>
        private static void SendMail(StringBuilder mailBody)
        {
            MailMessage email = new MailMessage();
            email.To.Add(ConfigurationSettings.AppSettings["NotificationEmail"]);
            email.Subject = "New attack(s) detected. IPs blocked.";
            email.From = new MailAddress(
                ConfigurationSettings.AppSettings["FromEmail"]);
            email.Sender = new MailAddress(
                ConfigurationSettings.AppSettings["FromEmail"]);
            email.Body = mailBody.ToString();
            email.IsBodyHtml = false;

            SmtpClient client = new SmtpClient(
                ConfigurationSettings.AppSettings["SmtpServer"]);
            client.UseDefaultCredentials = true;
            client.EnableSsl = bool.Parse(
                ConfigurationSettings.AppSettings["UseSslForSmtp"]);

            client.Send(email);
        }

        /// <summary>
        /// Runs one round of periodic processing.
        /// </summary>
        /// <param name="lastProcessedIndex"> Previously seen event index. Processes
        /// all events that are newer than this. </param>
        /// <returns> The new watermark for event index. </returns>
        private static int Process(int lastProcessedIndex)
        {

            StringBuilder mailBody = new StringBuilder();

            int failEvents = int.Parse(
                ConfigurationSettings.AppSettings["LogonFailuresPerPeriod"]);
            int failPeriod = 1000 * 60 * int.Parse(
                ConfigurationSettings.AppSettings["FailurePeriodMinutes"]);
            Regex knownGoodIps = BuildRegexForIpList(
                ConfigurationSettings.AppSettings["KnownGoodIpList"]);

            Dictionary<string, LinkedList<DateTime>> trackedIps =
                new Dictionary<string, LinkedList<DateTime>>();

            HashSet<string> blockedIps = GetCurrentlyBlockedIps();
            
            Dictionary<string, List<string>> auditFailures =
                new Dictionary<string, List<string>>();

            HashSet<string> newlyBlockedIps = new HashSet<string>();

            using (EventLog ev = new EventLog("Security"))
            {
                EventLogEntryCollection entries = ev.Entries;

                for (int index = entries.Count - 1; index >= 0; index--)
                {
                    EventLogEntry ele = entries[index];
                    if (ele.Index < lastProcessedIndex)
                        break;

                    if (ele.CategoryNumber != 2 || ele.InstanceId != 529 ||
                        ele.EntryType != EventLogEntryType.FailureAudit)
                        continue;

                    if (ele.ReplacementStrings.Length < 11 ||
                        knownGoodIps.IsMatch(ele.ReplacementStrings[11]))
                        continue;

                    string ip = ele.ReplacementStrings[11];

                    if (!auditFailures.ContainsKey(ip))
                        auditFailures[ip] = new List<string>();

                    auditFailures[ip].Add(ele.Message);

                    if (blockedIps.Contains(ip))
                        continue;

                    LinkedList<DateTime> queue;
                    if (!trackedIps.ContainsKey(ip))
                    {
                        queue = new LinkedList<DateTime>();
                        queue.AddFirst(ele.TimeGenerated);
                        trackedIps[ip] = queue;

                        continue;
                    }

                    queue = trackedIps[ip];
                    queue.AddFirst(ele.TimeGenerated);

                    if (queue.Count < failEvents)
                        continue;

                    while (queue.Count > failEvents)
                        queue.RemoveLast();

                    TimeSpan period = queue.Last.Value - queue.First.Value;
                    if (period.Milliseconds > failPeriod)
                        continue;

                    string msg = string.Format(
                        "{0} Adding the following ip to blocked list: {1}",
                        DateTime.Now, ip);

                    blockedIps.Add(ip);
                    newlyBlockedIps.Add(ip);

                    NetSh("routing ip add filter name=\"Network Connection\" filtertype=INPUT srcaddr="
                        + ip + " srcmask=255.255.255.255 dstaddr=0.0.0.0 dstmask=0.0.0.0 proto=ANY");

                    Console.WriteLine(msg);
                    mailBody.Append(msg);
                    mailBody.Append("\r\n");
                }

                lastProcessedIndex = entries[entries.Count - 1].Index;
            }

            if (mailBody.Length > 0)
            {
                foreach (string ip in newlyBlockedIps)
                {
                    foreach (string message in auditFailures[ip])
                    {
                        mailBody.Append("-----\r\n");
                        mailBody.Append(message);
                    }
                }

                SendMail(mailBody);
            }

            return lastProcessedIndex;
        }

        /// <summary>
        /// Main entry point, does all the work.
        /// </summary>
        /// <param name="args"> Program arguments. Not used, all configuration is
        /// through app.config. </param>
        static void Main(string[] args)
        {
            int lastProcessedIndex = 0;
            for (; ; )
            {
                lastProcessedIndex = Process(lastProcessedIndex);

                GC.Collect();

                int sleep = int.Parse(
                    ConfigurationSettings.AppSettings["SleepIntervalMinutes"]);
                Console.WriteLine("Sleeping for {0} minutes @ {1}...",
                    sleep, lastProcessedIndex);
                Thread.Sleep(sleep * 60 * 1000);
            }
        }
    }
}
It also requires an app.config with the following settings:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="KnownGoodIpList" value="192.168.,131.107."/>
    <add key="LogonFailuresPerPeriod" value="5" />
    <add key="FailurePeriodMinutes" value="2" />
    <add key="SmtpServer" value="xxx" />
    <add key="NotificationEmail" value="yyy" />
    <add key="FromEmail" value="zzz" />
    <add key="UseSslForSmtp" value="true" />
    <add key="SleepIntervalMinutes" value="2"/>
  </appSettings>
</configuration>

The whole thing took barely 2 hours, and have stopped short two attacks just this evening!