<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-1143415379421433159</id><updated>2011-12-07T14:05:12.264+01:00</updated><category term='wet'/><category term='running'/><category term='nokia'/><category term='symbian nokia'/><category term='dotNET'/><category term='symbian'/><category term='sports'/><title type='text'>Mind in the Water</title><subtitle type='html'>adrift in the sea of experience</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default?max-results=100'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>42</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-7114637530795207326</id><published>2011-05-10T02:00:00.000+02:00</published><updated>2011-11-30T12:08:26.720+01:00</updated><title type='text'>Problems encountered when using MEF as a dependency injection container</title><content type='html'>When we started using &lt;a href="http://mef.codeplex.com/"&gt;MEF&lt;/a&gt; as a dependency injection container, I figured the trade-off was a bit like this. &lt;b&gt;Good&lt;/b&gt;: Already part of the .NET 4 framework, dynamic discovery of components for extensibility. &lt;b&gt;Bad&lt;/b&gt;: missing some advanced features like AOP and parametrized construction. Glenn Block posted about this in &lt;a href="http://codebetter.com/glennblock/2009/08/16/should-i-use-mef-for-my-general-ioc-needs/"&gt;Should I use MEF for my general IoC needs?&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Since we didn't need those features, MEF seemed like a good choice.&amp;nbsp;But as it turns out, there is a more subtle problem when using MEF as a general purpose dependency injection container. Consider the following example:&lt;br /&gt;&lt;br /&gt;&lt;pre class="c#" name="code"&gt;   public class Program &lt;br /&gt;   {&lt;br /&gt;      static void Main(string[] args)&lt;br /&gt;      {&lt;br /&gt;         var container = new CompositionContainer(&lt;br /&gt;            new AssemblyCatalog(Assembly.GetExecutingAssembly()));&lt;br /&gt;         var a = container.GetExportedValue&amp;lt;A&amp;gt;();&lt;br /&gt;      }&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   [Export]&lt;br /&gt;   public class A&lt;br /&gt;   {&lt;br /&gt;      private readonly B b;&lt;br /&gt;&lt;br /&gt;      [ImportingConstructor]&lt;br /&gt;      public A(B b)&lt;br /&gt;      {&lt;br /&gt;         this.b = b;&lt;br /&gt;      }&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   [Export]&lt;br /&gt;   public class B&lt;br /&gt;   {&lt;br /&gt;      private readonly C c;&lt;br /&gt;&lt;br /&gt;      [ImportingConstructor]&lt;br /&gt;      public B(C c)&lt;br /&gt;      {&lt;br /&gt;         this.c = c;&lt;br /&gt;      }&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   public class C&lt;br /&gt;   {&lt;br /&gt;   }&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;The export attribute is missing on the C class in my example. Since class A indirectly depends on class C, we get an error when we try to get an A instance from the container:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;System.ComponentModel.Composition.ImportCardinalityMismatchException was unhandled&lt;br /&gt;  Message=No valid exports were found that match the constraint '((exportDefinition.ContractName == "DITest.A") AndAlso (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "DITest.A".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))', invalid exports may have been rejected.&lt;br /&gt;  Source=System.ComponentModel.Composition&lt;br /&gt;  StackTrace: ...&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://i.imagehost.org/0859/south-park-cartman-screw-you-guys.jpg" imageanchor="1" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"&gt;&lt;img border="0" src="http://i.imagehost.org/0859/south-park-cartman-screw-you-guys.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;So the C export is missing, but surprisingly the error message is complaining &lt;b&gt;about the A class&lt;/b&gt;! This is because of "stable composition". Basically, whenever a dependency for a certain part is missing, MEF will resiliently attempt to do the composition without that part. For an example of how that can be useful, see &lt;a href="http://blogs.msdn.com/b/nblumhardt/archive/2009/07/17/light-up-or-mef-optional-exports.aspx"&gt;Implementing Optional Exports with MEF Stable Composition&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Useful as it may be, stable composition comes at a price. Since MEF can't tell the difference between critical and non-critical parts, the missing dependency error may cascade upward until you get a mysterious ImportCardinalityMismatchException like the one above.&lt;br /&gt;&lt;br /&gt;The MEF documentation on &lt;a href="http://mef.codeplex.com/wikipage?title=Debugging%20and%20Diagnostics&amp;amp;referringTitle=Guide"&gt;Diagnosing Composition Problems&amp;nbsp;&lt;/a&gt;acknowledges this and provides some hints on how to debug such problems. Still, for large compositions the process is far from pleasant. Things are even worse if you have some circular dependencies.&lt;br /&gt;&lt;br /&gt;Perhaps it would be better to use &lt;a href="http://code.google.com/p/autofac/"&gt;Autofac&lt;/a&gt; to do the core application composition. It doesn't attempt dynamic discovery or stable composition, so the error messages point straight at the missing dependencies. And with the &lt;a href="http://code.google.com/p/autofac/wiki/MefIntegration"&gt;MEF integration&lt;/a&gt;, you can still use MEF for the parts of the application where you do need dynamic discovery and stable composition.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-7114637530795207326?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/7114637530795207326/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=7114637530795207326' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7114637530795207326'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7114637530795207326'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2011/05/problems-encountered-when-using-mef-as.html' title='Problems encountered when using MEF as a dependency injection container'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-7969007874348629602</id><published>2011-05-07T02:09:00.000+02:00</published><updated>2011-05-07T02:09:57.544+02:00</updated><title type='text'>DownMarker, a Markdown editor/viewer</title><content type='html'>I've released version 0.1 of&amp;nbsp;&lt;a href="http://bitbucket.org/wcoenen/downmarker"&gt;DownMarker&lt;/a&gt;, a small desktop application that can be used to to view, navigate and edit a set of interlinked markdown files:&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://dl.dropbox.com/u/119154/permalink/downmarker.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://dl.dropbox.com/u/119154/permalink/downmarker.png" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;In case you don't know &lt;a href="http://daringfireball.net/projects/markdown/"&gt;Markdown&lt;/a&gt;, it is a "easy-to-read, easy-to-write plain text format" intended to be converted to HTML. I first encountered it as the format used at&amp;nbsp;&lt;a href="http://stackoverflow.com/"&gt;Stackoverflow &lt;/a&gt;for editing questions and answers. In fact, DownMarker is based on MarkdownSharp, a library generously released as open source by the stackoverflow team.&lt;br /&gt;&lt;br /&gt;Currently some things are still missing, but my hope is that DownMarker will work well for creating light-weight wikis inside your version controlled projects, or anything else that can store files. Let me know if you find it useful!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-7969007874348629602?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/7969007874348629602/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=7969007874348629602' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7969007874348629602'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7969007874348629602'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2011/05/downmarker-markdown-editorviewer.html' title='DownMarker, a Markdown editor/viewer'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-2629021334953533548</id><published>2011-03-01T01:29:00.001+01:00</published><updated>2011-03-10T12:19:52.121+01:00</updated><title type='text'>MEF attribute-less registration</title><content type='html'>The Managed Extensibility Framework has shipped in .NET4, but the work hasn't stopped: the recent &lt;a href="http://mef.codeplex.com/releases/view/61730"&gt;MEF2-Preview3 release&lt;/a&gt; added some interesting stuff. Let's take a look at &lt;b&gt;attribute-less registration&lt;/b&gt;. &lt;br /&gt;&lt;br /&gt;First, a recap of the existing attribute-based registration mechanism (or "Attributed Programming Model" in MEF-parlance):&lt;br /&gt;&lt;br /&gt;&lt;center&gt;&lt;img src="http://dl.dropbox.com/u/119154/permalink/mef-attributed.png" /&gt;&lt;/center&gt;&lt;br /&gt;&lt;br /&gt;A composition container can take different kinds of export providers. The diagram shows the catalog-based export provider. &lt;tt&gt;TypeCatalog&lt;/tt&gt; is the most important catalog implementation: it is based on a list of types, which it inspects via reflection for MEF attributes. The other implementations are there for convenience, to quickly build type catalogs out of an assembly (&lt;tt&gt;AssemblyCatalog&lt;/tt&gt;), an entire directory (&lt;tt&gt;DirectoryCatalog&lt;/tt&gt;), or other catalogs (&lt;tt&gt;AggregateCatalog&lt;/tt&gt;).&lt;br /&gt;&lt;br /&gt;Here is an illustration of the above with a quick code example featuring some dummy types. (The &lt;tt&gt;CatalogExportProvider&lt;/tt&gt; is not visible because the &lt;tt&gt;CompositionContainer&lt;/tt&gt; can conveniently construct it for us if we directly pass it a catalog.)&lt;br /&gt;&lt;pre name="code" class="c#"&gt;interface IFoo { }&lt;br /&gt;   interface IBar { }&lt;br /&gt;&lt;br /&gt;   [Export(typeof(IFoo))]&lt;br /&gt;   class Foo : IFoo&lt;br /&gt;   {&lt;br /&gt;      [Import(typeof(IBar))]&lt;br /&gt;      public IBar Bar { get; set; }&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   [Export(typeof(IBar))]&lt;br /&gt;   public class Bar : IBar { }&lt;br /&gt;&lt;br /&gt;   class Program&lt;br /&gt;   {&lt;br /&gt;      static void Main(string[] args)&lt;br /&gt;      {&lt;br /&gt;         var catalog = new TypeCatalog(typeof(Foo), typeof(Bar));&lt;br /&gt;         var container = new CompositionContainer(catalog);&lt;br /&gt;         var foo = container.GetExportedValue&amp;lt;IFoo&amp;gt;();&lt;br /&gt;      }&lt;br /&gt;   }&lt;/pre&gt;&lt;br /&gt;For an attribute-free alternative to the above, you might expect that the new MEF release would contain an alternative &lt;tt&gt;ExportProvider&lt;/tt&gt; or perhaps a new &lt;tt&gt;ComposablePartCatalog&lt;/tt&gt; implementation, which would take its information from configuration in code instead of reflection. Surprisingly, that's not how it was done!&lt;br /&gt;&lt;br /&gt;In reality, the attribute-less registration is implemented &lt;b&gt;by adding information right above the reflection level&lt;/b&gt;! We essentially inject fake reflection information into our catalogs to simulate the presence of attributes. The attribute-free version of the above example looks like this:&lt;br /&gt;&lt;br /&gt;&lt;pre name="code" class="c#"&gt;interface IFoo { }&lt;br /&gt;   interface IBar { }&lt;br /&gt;&lt;br /&gt;   class Foo : IFoo &lt;br /&gt;   {&lt;br /&gt;      public IBar Bar { get; set; }&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   class Bar : IBar { }&lt;br /&gt;&lt;br /&gt;   class Program&lt;br /&gt;   {&lt;br /&gt;      static void Main(string[] args)&lt;br /&gt;      {&lt;br /&gt;         var registration = new RegistrationBuilder();&lt;br /&gt;&lt;br /&gt;         registration.OfType&amp;lt;Foo&amp;gt;()&lt;br /&gt;            .ImportProperty&amp;lt;IBar&amp;gt;(property =&gt; property.Name == "Bar")&lt;br /&gt;            .Export&amp;lt;IFoo&amp;gt;();&lt;br /&gt;&lt;br /&gt;         registration.OfType&amp;lt;Bar&amp;gt;()&lt;br /&gt;            .Export&amp;lt;IBar&amp;gt;();&lt;br /&gt;&lt;br /&gt;         var catalog = new TypeCatalog(&lt;br /&gt;             types: new Type[] { typeof(Foo), typeof(Bar) },&lt;br /&gt;             reflectionContext: registration);&lt;br /&gt;         var container = new CompositionContainer(catalog);&lt;br /&gt;         var foo = container.GetExportedValue&amp;lt;IFoo&amp;gt;();&lt;br /&gt;      }&lt;br /&gt;   }&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;Note how the &lt;tt&gt;TypeCatalog&lt;/tt&gt; takes a new &lt;tt&gt;ReflectionContext&lt;/tt&gt; parameter; that's were the reflection info is injected. An interesting result of this approach is that you can leverage the existing catalogs to register types in bulk. For example, the following example will export all the types which implement &lt;tt&gt;IPlugin&lt;/tt&gt; in a given directory, without using any attributes:&lt;br /&gt;&lt;pre name="code" class="c#"&gt;var registration = new RegistrationBuilder();&lt;br /&gt;registration.Implements&amp;lt;IPlugin&amp;gt;().Export&amp;lt;IPlugin&amp;gt;();&lt;br /&gt;var catalog = new DirectoryCatalog("./plugins", "*.dll", registration);&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;This is just what I've gleaned from a quick peek at the unit tests in the new MEF release. There is much more to the RegistrationBuilder API which I haven't shown here. I'm sure there will be more documentation on the &lt;a href="http://mef.codeplex.com"&gt;MEF codeplex site&lt;/a&gt; shortly. Also, keep in mind that the APIs in preview releases are subject to change.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;UPDATE&lt;/b&gt;: The MEF product manager, Hamilton Verissimo (aka "hammet" and "haveriss"), has blogged about &lt;a href="http://blogs.msdn.com/b/hammett/archive/2011/03/08/mef-s-convention-model.aspx"&gt;MEF's convention model&lt;/a&gt;.&lt;br/&gt;&lt;br/&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-2629021334953533548?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/2629021334953533548/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=2629021334953533548' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2629021334953533548'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2629021334953533548'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2011/03/mef-attribute-less-registration.html' title='MEF attribute-less registration'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-1114733288216815003</id><published>2011-02-25T19:50:00.005+01:00</published><updated>2011-04-08T21:56:19.405+02:00</updated><title type='text'>Executing visual studio 2010 unit tests without installing visual studio</title><content type='html'>In a &lt;a href="http://mindinthewater.blogspot.com/2008/11/executing-visual-studio-unit-tests.html"&gt;previous post&lt;/a&gt; I already explained how to get your unit tests running on a build machine with mstest.exe, without having to install Visual Studio 2008. We recently upgraded to Visual Studio 2010 so I had to repeat the exercise. &lt;br /&gt;&lt;br /&gt;This time I wrote a &lt;a href="http://dl.dropbox.com/u/119154/permalink/getmstest.bat"&gt;batch script&lt;/a&gt; which does most of the work. Just run the script on a machine where Visual Studio 2010 is installed, and it will create a "mstest" folder with the necessary binaries and registry entries. Copy the folder to your build machine, prepare the registry by executing mstest.reg and you're good to run mstest.exe like this:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;mstest /noisolation /testcontainer:"path\to\testproject.dll"&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;This will return an error code if any of the tests fail.&lt;br /&gt;&lt;br /&gt;As an anonymous commenter on my previous post pointed out, you should be careful of the license implications.  In our case it is most likely OK because we have a site license. But even so it can be useful to avoid a full Visual Studio install: it saves a lot of disk space, and you don't have to spend time babysitting the installation. Or multiple installations, if you have a cluster of build machines!&lt;br /&gt;&lt;br /&gt;&lt;b&gt;update&lt;/b&gt;: thanks to &lt;a href="http://www.incisif.net/"&gt;Frederic Torres&lt;/a&gt; for pointing out that the script doesn't work on a 64-bit Windows, and suggesting a fix! &lt;strike&gt;It should work now.&lt;/strike&gt; Apparently there are still problems with 64-bit machines. But I don't have a 64-bit machine without VS2010 to test with, so I'll have to leave it as an exercise for the reader...&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-1114733288216815003?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/1114733288216815003/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=1114733288216815003' title='9 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/1114733288216815003'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/1114733288216815003'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2011/02/executing-visual-studio-2010-unit-tests.html' title='Executing visual studio 2010 unit tests without installing visual studio'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>9</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-7291560767238951424</id><published>2010-12-01T01:41:00.002+01:00</published><updated>2010-12-01T01:45:26.065+01:00</updated><title type='text'>bisect your source code history to find the problematic revision</title><content type='html'>I am working on a little &lt;a href="http://bitbucket.org/wcoenen/downmarker"&gt;application&lt;/a&gt;&amp;nbsp;to edit &lt;a href="http://en.wikipedia.org/wiki/Markdown"&gt;Markdown&lt;/a&gt; documents with instant preview. (For those of you who aren't into building things from source, here's a&amp;nbsp;&lt;a href="http://dl.dropbox.com/u/119154/permalink/downmarker.msi"&gt;windows installer&lt;/a&gt; to put a downmarker shortcut in your start menu.)&amp;nbsp;I like to use both windows and linux so I try to make sure that it runs on both &lt;a href="http://en.wikipedia.org/wiki/.NET_Framework"&gt;Microsoft.NET&lt;/a&gt; and &lt;a href="http://en.wikipedia.org/wiki/Mono_(software)"&gt;Mono&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;When I last tested my application on Mono, I discovered a very annoying problem: each time I tried to type something in a markdown document, a new nautilus file browser window would be launched!&amp;nbsp;I had absolutely no idea why this would happen or where to start debugging. To make matters worse, I hadn't tested on Mono for about 40 revisions so there were a lot of possible changes that might have introduced the problem.&lt;br /&gt;&lt;br /&gt;I have the source code history in a &lt;a href="http://mercurial.selenic.com/"&gt;mercurial&lt;/a&gt; repository, so I decided to give the "bisect" feature a try. I started by marking the latest revision as "known bad":&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;hg bisect --bad&lt;/blockquote&gt;&lt;br /&gt;I also remembered making some mono-specific bug fixes, and the problem didn't exist at that point. So I grepped the output of hg log for commit messages containing the word "mono" and marked the revision as good:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;hg bisect --good 627d6&lt;/blockquote&gt;&lt;br /&gt;At this point the bisect command has a revision range to work with. It will automatically update your working copy to a revision right in the middle of that range. You just have to rebuild your application, verify whether the problem is still there, and mark the revision as good or bad. (It is also possible to do this test automatically with a script.) The bisect command will then automatically cut the revision range to investigate in half again, and select yet another revision in the middle, etcetera.&lt;br /&gt;&lt;br /&gt;Sure enough, after about 5 or 6 tests I got this message:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;The first bad revision is:&lt;br /&gt;changeset: &amp;nbsp; 42:c4bbabe79dde&lt;br /&gt;user: &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;wcoenen&lt;br /&gt;date: &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Sun Nov 07 23:31:32 2010 +0100&lt;br /&gt;summary: &amp;nbsp; &amp;nbsp; Links are now handled by opening .md or by external application.&lt;/blockquote&gt;&lt;br /&gt;Aha! Looks like I added some code to handle link clicks by passing the URL to the OS (to open it with a suitable external application). And obviously this code is now being triggered erroneously on mono whenever the document is updated. Thank you bisect!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-7291560767238951424?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/7291560767238951424/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=7291560767238951424' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7291560767238951424'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7291560767238951424'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/12/bisect-your-source-code-history-to-find.html' title='bisect your source code history to find the problematic revision'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-6521575242492357133</id><published>2010-11-24T11:48:00.000+01:00</published><updated>2010-11-24T11:48:08.981+01:00</updated><title type='text'>git error: error setting certificate verify locations</title><content type='html'>I was trying to clone a repository from &lt;a href="https://github.com/"&gt;github&lt;/a&gt;&amp;nbsp;when I ran into this error:&lt;br /&gt;&lt;blockquote&gt;Cloning into docu...&lt;br /&gt;error: error setting certificate verify locations:&lt;br /&gt;&amp;nbsp;&amp;nbsp;CAfile: \bin\curl-ca-bundle.crt&lt;br /&gt;&amp;nbsp;&amp;nbsp;CApath: none&lt;br /&gt;&amp;nbsp;while accessing https://github.com/jagregory/docu/info/refs&lt;/blockquote&gt;&lt;br /&gt;If you google around, many people "solve" this by disabling the SSL certificate check entirely. Obviously there is a reason for that check, so disabling it is not quite the right solution!&amp;nbsp;It turns out that there is mistake in the gitconfig file that comes with msgit setup (I have Git-1.7.2.3 installed).&amp;nbsp;The right fix is to change the sslCAinfo setting in "c:\program files\git\etc\gitconf" to this:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;sslCAinfo = c:\\program files\\git\\bin\\curl-ca-bundle.crt&lt;/blockquote&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-6521575242492357133?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/6521575242492357133/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=6521575242492357133' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/6521575242492357133'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/6521575242492357133'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/11/git-error-error-setting-certificate.html' title='git error: error setting certificate verify locations'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-4625313563368249299</id><published>2010-11-23T11:59:00.000+01:00</published><updated>2010-11-23T11:59:26.165+01:00</updated><title type='text'>Mercurial and subversion subrepos</title><content type='html'>It is not yet mentioned in the &lt;a href="http://hgbook.red-bean.com/"&gt;Mercurial book&lt;/a&gt;, but Mercurial has a &lt;a href="http://mercurial.selenic.com/wiki/Subrepository"&gt;subrepos feature&lt;/a&gt; to pull code from other repositories into your project as a nested repository. It is a bit similar to SVN externals and git submodules.&lt;br /&gt;&lt;br /&gt;Better yet,&lt;a href="http://mercurial.selenic.com/wiki/Subrepository#SVN_subrepositories"&gt; it also works with subversion&lt;/a&gt;!&amp;nbsp;There are still some bugs to be worked out though: you better not move your SVN subrepo around in your mercurial repository. For all the ugly details, see my &lt;a href="http://mercurial.selenic.com/bts/issue2515"&gt;bug report&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-4625313563368249299?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/4625313563368249299/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=4625313563368249299' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/4625313563368249299'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/4625313563368249299'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/11/mercurial-and-subversion-subrepos.html' title='Mercurial and subversion subrepos'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-8367538211724243989</id><published>2010-11-19T12:45:00.002+01:00</published><updated>2010-11-19T12:47:49.184+01:00</updated><title type='text'>An equivalent of .bashrc for the windows cmd.exe shell</title><content type='html'>Tired of the "cd" command in windows refusing to navigate to folders on other drives? Put this in the [HKEY_CURRENT_USER\Software\Microsoft\Command Processor] AutoRun key:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;c:\cmdauto.cmd&lt;/blockquote&gt;&lt;br /&gt;This will cause the given script to be executed each time a cmd.exe window is opened. I found out about that thanks to &lt;a href="http://superuser.com/questions/144347/is-there-windows-equivalent-of-the-bashrc-file-in-linux"&gt;this superuser.com post&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Now put this in c:\cmdauto.cmd&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;@echo off&lt;br /&gt;doskey cd=pushd $*&lt;/blockquote&gt;&lt;br /&gt;The doskey command creates aliases, here overriding the behavior of "cd".&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-8367538211724243989?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/8367538211724243989/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=8367538211724243989' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/8367538211724243989'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/8367538211724243989'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/11/bashrc-for-windows-cmdexe-shell.html' title='An equivalent of .bashrc for the windows cmd.exe shell'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-8527115856759788379</id><published>2010-06-23T18:42:00.000+02:00</published><updated>2010-06-23T18:42:06.172+02:00</updated><title type='text'>ZFS-Fuse reliability report</title><content type='html'>A few weeks ago, I got this question in the comments on my &lt;a href="http://mindinthewater.blogspot.com/2010/02/building-nas-part-7-zfs-snapshots.html"&gt;last NAS post&lt;/a&gt;:&lt;br /&gt;&lt;blockquote&gt;Since you've been using ZFS-fuse for a time now, can you report here or blog about its stability? Has the daemon crashed on you, while taking a snapshot, or while "scrub"-ing?&lt;br /&gt;&lt;br /&gt;How much data have you passed in? &lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;First, note that the version of ZFS-FUSE which I am using is not a particular release, but &lt;a href="http://gitweb.zfs-fuse.net/?p=official;a=commit;h=0105e9261ce0da7d634bfae86035f17e0d437ac8"&gt;this revision&lt;/a&gt; from the official git repository. I have not bothered to update my ZFS binaries since January, because I didn't encounter any problems.&lt;br /&gt;&lt;br /&gt;The "zfs list" output for my pool shows that I am using 352 GiB. The pool consists of two 465 GiB disks in a mirror setup.&lt;br /&gt;&lt;pre&gt;NAME               USED  AVAIL  REFER  MOUNTPOINT&lt;br /&gt;nas-pool           352G   105G    24K  /nas-pool&lt;br /&gt;nas-pool/archive   118G   105G  71.1G  /nas-pool/archive&lt;br /&gt;nas-pool/bulk      234G   105G   228G  /nas-pool/bulk&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;The pool is scrubbed by a cron script each sunday night. I have not seen any crashes/hangs during scrubbing. The scrubs have not detected any errors so far.&lt;br /&gt;&lt;br /&gt;"archive" is snapshotted every night. "bulk" is snapshotted" only on sunday nights. The pool contains more than a hundred snapshots. Again, I have not seen any crashes or hangs.&lt;br /&gt;&lt;br /&gt;Then there is read/write activity: I use it to automatically archive my emails from Gmail (nightly), sync with my dropbox folder (continuously), and to store my mp3s, photos, videos, downloads and backups when I need to. When reading or writing NAS files, I am usually working from my laptop over WiFi which is a speed bottleneck, so this probably doesn't really stress the file system. On the other hand, I do regularly make a full off-line backup of the "archive" filesystem by hooking up an external USB disk to the NAS. I haven't seen ZFS crash during any of this.&lt;br /&gt;&lt;br /&gt;From this, I conclude that ZFS-Fuse is pretty stable.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-8527115856759788379?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/8527115856759788379/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=8527115856759788379' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/8527115856759788379'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/8527115856759788379'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/06/zfs-fuse-reliability-report.html' title='ZFS-Fuse reliability report'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-7190751499571516637</id><published>2010-05-29T02:49:00.000+02:00</published><updated>2010-05-29T02:49:23.173+02:00</updated><title type='text'>Exponential growth has limits</title><content type='html'>I saw this &lt;a href="http://science.slashdot.org/comments.pl?sid=1666248&amp;cid=32358368"&gt;comment&lt;/a&gt; in the thread of a &lt;a href="http://science.slashdot.org/story/10/05/27/0258245/Sudden-Demand-For-Logicians-On-Wall-Street"&gt;slashdot post about Wall Street&lt;/a&gt; today:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;&lt;p&gt;But everything thus far shows us that perpetual growth is possible. Technology is a wonderful thing - each year we're able to do more with less.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;That's not to say that a lot of what goes on in the market isn't pure, unadulterated bullshit, but real, honest-to-goodness "growth" won't stop until technology does.&lt;br /&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;This is a textbook example of the "cornucopian" view on economics. The implicit assumption here is that there are no limits to technological improvements, or that those limits are extremely far in our future. I think that this assumption is flat out wrong and dangerous. It is these sorts of ideas which have lead large parts of the global economy to eerily resemble &lt;a href="http://en.wikipedia.org/wiki/Ponzi_scheme"&gt;Ponzi Schemes&lt;/a&gt;. &lt;br /&gt;&lt;/p&gt;&lt;p&gt;I wrote the following &lt;a href="http://science.slashdot.org/comments.pl?sid=1666248&amp;cid=32374378"&gt;post&lt;/a&gt; in reply:&lt;br /&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;There are limits to exponential growth. (And make no mistake, growth expressed as a fixed percentage per year is exponential). Technology can push the limits closer to what the laws of physics allow, but technology cannot change the laws of physics.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Let's look at some numbers to drive the point home. Our &lt;a href="http://en.wikipedia.org/wiki/World_energy_resources_and_consumption"&gt;global energy consumption&lt;/a&gt; in 2008 was estimated to be 474 exajoules.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;The total energy received by the earth from the sun during a year is about 5 million exajoules, a fraction of which reaches the surface. 5 million is much more than 474. But at a seemingly modest 2% per year growth rate (as it was between 1980 and 2006), our energy consumption will match those 5 million exajoules &lt;b&gt;in less than 500 years&lt;/b&gt;!&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Think about that: if energy consumption growth continues at the current pace, then in 500 years we'll either be using ALL solar energy received by the earth (leaving none for the biosphere), or we'll have figured out some magic technology to produce 5 million exajoules of energy per year. Assuming the magic technology, where are we going to get rid of all that extra heat? It would effectively be like having a second sun on earth, cooking us in place.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Granted, you did say "do more with less". So lets say energy consumption will stay constant in the future, and instead we'll derive 2% more "value" from the same energy each year. Now you run into a new problem. No matter how you define "value", you run into physical limits. If you define value as "amount of mass lifted out of the earth's gravity field", then the hard efficiency limit is a minimum of 60 megajoules per kg. If you define value as "amount of computation", then again &lt;a href="http://en.wikipedia.org/wiki/Limits_to_computation"&gt;there are limits&lt;/a&gt; given by the laws of physics.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Exponential growth is counterintuitive. No matter how far you push the limits (e.g. by colonizing the entire galaxy or inventing game-changing technology), exponential growth will hit its limits much faster than you think. We're talking about growth with a fixed doubling period here.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Finally, I'd argue that we are already experiencing the end of exponential growth today. After decades of growth, &lt;a href="http://europe.theoildrum.com/node/6477"&gt;in 2004 global oil production reached a plateau&lt;/a&gt; [theoildrum.com]. It's not a coincidence that we experienced a major financial crash and recession soon after that. The era of "perpetual growth" is over. The next era will be that of the "zero-sum game" at best.&lt;br /&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;On a related note, after writing the above post I stumbled on this excellent &lt;a href="http://www.youtube.com/watch?v=F-QA2rkpBSY"&gt;series of videos of a lecture by Dr. Albert A. Bartlett&lt;/a&gt; where he shows with some very simple calculations and examples that "steady growth" is in fact always unsustainable. We have to find a way to get our civilization to work with 0% growth. The alternative is total collapse, probably within decades.&lt;br /&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-7190751499571516637?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/7190751499571516637/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=7190751499571516637' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7190751499571516637'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7190751499571516637'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/05/exponential-growth-has-limits.html' title='Exponential growth has limits'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-5214398621508344136</id><published>2010-04-03T04:16:00.004+02:00</published><updated>2010-04-03T04:41:52.422+02:00</updated><title type='text'>Building a dependency injection container in 30 lines</title><content type='html'>After reading &lt;a href="http://www.codeproject.com/KB/WPF/MessageBoxInMVVM.aspx?msg=3425908#xx3425908xx"&gt;this article&lt;/a&gt; by Josh Smith explaining the Service Locator pattern, I commented that direct dependency injection might be a better idea. Mark Seemann has a good write up about why &lt;a href="http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx"&gt;Service Locator is an anti-Pattern&lt;/a&gt;, and I'm inclined to agree with him.&lt;br /&gt;&lt;br /&gt;When Josh replied that injecting dependencies with constructor arguments doesn't really solve the problem of dependency creation, I was tempted to reply by enumerating all the .NET dependency injection frameworks that exist for exactly this purpose. &lt;br /&gt;&lt;br /&gt;But then I realized that Josh had demonstrated the Service Locator pattern without using any framework. Instead, his article has a ServiceContainer class of about 30 lines. Service Locator has many disadvantages, but apparently it can be quite lightweight!&lt;br /&gt;&lt;br /&gt;This then lead me to wonder if the same could be done for creating a dependency injection framework. Ayende has actually already demonstrated that you can create a primitive one in &lt;a href="http://ayende.com/Blog/archive/2007/10/20/Building-an-IoC-container-in-15-lines-of-code.aspx"&gt;15 lines&lt;/a&gt;, but I was thinking of something that could be used with a more friendly &lt;a href="http://ninject.org/"&gt;Ninject&lt;/a&gt;-esque syntax like this:&lt;br /&gt;&lt;br /&gt;&lt;pre name="code" class="c#"&gt;var container = new Container();&lt;br /&gt;container.Bind&amp;lt;App, App&amp;gt;();&lt;br /&gt;container.Bind&amp;lt;IFoo, Foo&amp;gt;();&lt;br /&gt;container.Bind&amp;lt;IBar, Bar&amp;gt;();&lt;br /&gt;&lt;br /&gt;var app = container.Pull&amp;lt;App&amp;gt;();&lt;br /&gt;app.Run();&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;As it turns out, implementing a bare bones container which can do that is really not that hard. It also has the advantage that it takes care of the dependencies of the dependencies etcetera, something which Josh's sample doesn't seem to do. (Disclaimer: I didn't really test this for anything but the plain vanilla use case, no error conditions were considered.)&lt;br /&gt;&lt;br /&gt;&lt;pre name="code" class="c#"&gt;public class Container&lt;br /&gt;{&lt;br /&gt;    private readonly Dictionary&amp;lt;Type, Type&amp;gt; contractToClassMap = new Dictionary&amp;lt;Type, Type&amp;gt;();&lt;br /&gt;    private readonly Dictionary&amp;lt;Type, object&amp;gt; contractToInstanceMap = new Dictionary&amp;lt;Type, object&amp;gt;();&lt;br /&gt;&lt;br /&gt;    public void Bind&amp;lt;TContract, TClass&amp;gt;() where TClass : class, TContract&lt;br /&gt;    {&lt;br /&gt;        this.contractToClassMap[typeof(TContract)] = typeof(TClass);&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    public TContract Pull&amp;lt;TContract&amp;gt;()&lt;br /&gt;    {&lt;br /&gt;        return (TContract)Pull(typeof(TContract));&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    public object Pull(Type contract)&lt;br /&gt;    {&lt;br /&gt;        object instance;&lt;br /&gt;        this.contractToInstanceMap.TryGetValue(contract, out instance);&lt;br /&gt;        if (instance == null)&lt;br /&gt;        {&lt;br /&gt;            var constructor = contractToClassMap[contract].GetConstructors()[0];&lt;br /&gt;            var args = &lt;br /&gt;                from parameter in constructor.GetParameters() &lt;br /&gt;                select Pull(parameter.ParameterType);&lt;br /&gt;            instance = constructor.Invoke(args.ToArray());&lt;br /&gt;            this.contractToInstanceMap[contract] = instance;&lt;br /&gt;        }&lt;br /&gt;        return instance;&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-5214398621508344136?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/5214398621508344136/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=5214398621508344136' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5214398621508344136'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5214398621508344136'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/04/building-dependency-injection-container.html' title='Building a dependency injection container in 30 lines'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-4568607727836138988</id><published>2010-04-01T12:57:00.001+02:00</published><updated>2010-04-01T14:19:17.332+02:00</updated><title type='text'>Adding some compiler verification to PropertyChanged events</title><content type='html'>I've been playing around with Windows Presentation Foundation and the Model-View-ViewModel pattern in the past week. &lt;a href="http://msdn.microsoft.com/en-us/magazine/dd419663.aspx"&gt;Josh Smith's introductory article&lt;/a&gt; does a good job explaining how the MVVM pattern can be used used in WPF.&lt;br /&gt;&lt;br /&gt;One aspect of the pattern is that your view model needs to provide change notifications, for example by implementing the &lt;b&gt;INotifyPropertyChanged&lt;/b&gt; interface. To avoid repeating the same code, it might be a good idea to implement this in a shared base class:&lt;br /&gt;&lt;br /&gt;&lt;pre name="code" class="c#"&gt;public abstract class ViewModelBase : INotifyPropertyChanged&lt;br /&gt;   {&lt;br /&gt;      protected virtual void OnPropertyChanged(string propertyName)&lt;br /&gt;      {&lt;br /&gt;         if (PropertyChanged != null)&lt;br /&gt;         {&lt;br /&gt;            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));&lt;br /&gt;         }&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      public event PropertyChangedEventHandler PropertyChanged;&lt;br /&gt;   }&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;There is a problem with this though: &lt;b&gt;the caller of &lt;tt&gt;OnPropertyChanged&lt;/tt&gt; can pass any string&lt;/b&gt;. To constrain this to strings that match a property name, we can use reflection to check whether such a property indeed exists. The sample in Josh Smith's introductory article takes this approach. That way, passing an invalid string will at least raise an exception at run-time.&lt;br /&gt;&lt;br /&gt;However, we can still do one better and &lt;b&gt;catch such errors at compile time&lt;/b&gt;, which is a huge advantage during refactorings. The solution involves two advanced C# tricks. The first trick makes use of the fact that the C# compiler can convert lambdas into expression trees, which can then be inspected to extract the name of a property:&lt;br /&gt;&lt;br /&gt;&lt;pre name="code" class="c#"&gt;Foo foo = new Foo();&lt;br /&gt;    string propertyName = GetPropertyNameFromExpression&amp;lt;Foo,int&amp;gt;(x =&gt; x.Bar);&lt;br /&gt;    Debug.Assert(propertyName == "Bar");&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;The &lt;tt&gt;GetPropertyNameFromExpression&lt;/tt&gt; method is implemented like this:&lt;br /&gt;&lt;br /&gt;&lt;pre name="code" class="c#"&gt;private string GetPropertyNameFromExpression&amp;lt;TClass,TProperty&amp;gt;(&lt;br /&gt;         Expression&amp;lt;Func&amp;lt;TClass, TProperty&amp;gt;&amp;gt; expression)&lt;br /&gt;      {&lt;br /&gt;         var memberExpression = expression.Body as MemberExpression;&lt;br /&gt;         if (memberExpression == null)&lt;br /&gt;            throw new ArgumentException(String.Format(&lt;br /&gt;               "'{0}' is not a member expression", expression.Body));&lt;br /&gt;         return memberExpression.Member.Name;&lt;br /&gt;      }&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;But how do we get that &lt;tt&gt;TClass&lt;/tt&gt; type parameter in our base class? That's our second trick: as it turns out, it is possible for a base class to have a type parameter representing its derived classes. The resulting base class looks like this:&lt;br /&gt;&lt;br /&gt;&lt;pre name="code" class="c#"&gt;public abstract class ViewModelBase&amp;lt;TDerived&amp;gt; : INotifyPropertyChanged&lt;br /&gt;       where TDerived : ViewModelBase&amp;lt;TDerived&amp;gt;&lt;br /&gt;    {&lt;br /&gt;&lt;br /&gt;        private string GetPropertyNameFromExpression&amp;lt;TPropertyType&amp;gt;(&lt;br /&gt;           Expression&amp;lt;Func&amp;lt;TDerived, TPropertyType&amp;gt;&amp;gt; expression)&lt;br /&gt;        {&lt;br /&gt;            var memberExpression = expression.Body as MemberExpression;&lt;br /&gt;            if (memberExpression == null)&lt;br /&gt;                throw new ArgumentException(String.Format(&lt;br /&gt;                    "'{0}' is not a member expression", expression.Body));&lt;br /&gt;            return memberExpression.Member.Name;&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        /// &amp;lt;summary&amp;gt;&lt;br /&gt;        /// Triggers the &amp;lt;see cref="PropertyChanged"/&amp;gt; event for the property used in&lt;br /&gt;        /// &amp;lt;paramref name="expression"/&amp;gt;&lt;br /&gt;        /// &amp;lt;/summary&amp;gt;&lt;br /&gt;        /// &amp;lt;param name="expression"&amp;gt;&lt;br /&gt;        /// A simple member expression which uses the property to trigger the event for, e.g.&lt;br /&gt;        /// &amp;lt;c&gt;x =&gt; x.Foo&amp;lt;/c&amp;gt; will raise the event for the property "Foo".&lt;br /&gt;        /// &amp;lt;/param&amp;gt;&lt;br /&gt;        protected void OnPropertyChanged&amp;lt;TPropertyType&amp;gt;(&lt;br /&gt;            Expression&amp;lt;Func&amp;lt;TDerived,TPropertyType&amp;gt;&amp;gt; expression)&lt;br /&gt;        {&lt;br /&gt;            string name = GetPropertyNameFromExpression(expression);&lt;br /&gt;            if (PropertyChanged != null)&lt;br /&gt;                PropertyChanged(this, new PropertyChangedEventArgs(name));&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        public event PropertyChangedEventHandler PropertyChanged;&lt;br /&gt;    }&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;The viewmodel implementations then look like this:&lt;br /&gt;&lt;br /&gt;&lt;pre name="code" class="c#"&gt;public class FooViewModel : ViewModelBase&amp;lt;FooViewModel&amp;gt;&lt;br /&gt;   {&lt;br /&gt;      private int bar;&lt;br /&gt;&lt;br /&gt;      public int Bar&lt;br /&gt;      {&lt;br /&gt;         get&lt;br /&gt;         {&lt;br /&gt;            return this.bar;&lt;br /&gt;         }&lt;br /&gt;         set&lt;br /&gt;         {&lt;br /&gt;            this.bar = value;&lt;br /&gt;            OnPropertyChanged(x =&gt; x.Bar);&lt;br /&gt;         }&lt;br /&gt;      }&lt;br /&gt;   }&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;It is now much harder to trigger a &lt;tt&gt;PropertyChanged&lt;/tt&gt; event with the wrong property name, as the compiler will verify that the property actually exists. Better yet, if we do a "rename" refactoring of our properties then the IDE will also do the rename in the &lt;tt&gt;OnPropertyChanged&lt;/tt&gt; lambdas. Hurray! :-)&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-4568607727836138988?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/4568607727836138988/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=4568607727836138988' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/4568607727836138988'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/4568607727836138988'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/04/adding-some-compiler-verification-to.html' title='Adding some compiler verification to PropertyChanged events'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-2665006498657429400</id><published>2010-03-28T23:51:00.000+02:00</published><updated>2010-03-28T23:51:50.341+02:00</updated><title type='text'>The General Public License doesn't always force you to make your code available</title><content type='html'>I just answered this &lt;a href="http://stackoverflow.com/questions/2534205/practical-ways-around-the-gpl"&gt;question on stackoverflow asking for "ways around the GPL"&lt;/a&gt;. While the motives of the poster seem questionable, this did prompt me to explain a subtlety of the &lt;a href="http://www.gnu.org/licenses/gpl.html"&gt;General Public License&lt;/a&gt; which he may not have understood: the GPL does not&amp;nbsp;automatically force you to release your code as soon as you use GPL'ed code.&lt;br /&gt;&lt;br /&gt;The GPL only requires that you make the code available &lt;b&gt;to anyone you distribute the software to&lt;/b&gt;. This is not exactly the same as releasing it to the whole world for free. From the &lt;a href="http://www.gnu.org/licenses/gpl-faq.html#GPLRequireSourcePostedPublic"&gt;GPL FAQ&lt;/a&gt;:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;&lt;i&gt;Does the GPL require that source code of modified versions be posted to the public?&lt;/i&gt;&lt;br /&gt;&lt;br /&gt;The GPL does not require you to release your modified version, or any part of it. You are free to make modifications and use them privately, without ever releasing them. This applies to organizations (including companies), too; an organization can make a modified version and use it&lt;br /&gt;internally without ever releasing it outside the organization.&lt;br /&gt;&lt;br /&gt;But if you release the modified version to the public in some way, the GPL requires you to make the modified source code available to the program'susers, under the GPL.&lt;br /&gt;&lt;br /&gt;Thus, the GPL gives permission to release the modified program in certain ways, and not in other ways; but the decision of whether to release it is up to you.&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;So if you use GPL code for building an internal tool which you will never distribute to third parties, then there is no requirement to distribute the source to anyone.&lt;br /&gt;&lt;br /&gt;Interestingly enough, the above subtlety also applies if you only make the software available as &lt;b&gt;a web application hosted on your own web servers&lt;/b&gt;. Since you aren't technically distributing the application to the users, you don't have to give them the code. The&amp;nbsp;&lt;a href="http://www.affero.org/oagpl.html"&gt;Affero General Public License&lt;/a&gt;&amp;nbsp;(AGPL) was designed specifically to close this loophole.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-2665006498657429400?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/2665006498657429400/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=2665006498657429400' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2665006498657429400'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2665006498657429400'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/03/general-public-license-doesnt-always.html' title='The General Public License doesn&apos;t always force you to make your code available'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-5317833679707044975</id><published>2010-02-28T15:59:00.003+01:00</published><updated>2010-02-28T16:17:08.228+01:00</updated><title type='text'>Mocking frameworks: stubs vs mocks</title><content type='html'>I just answered another &lt;a href="http://stackoverflow.com/questions/2349454/rhinomocks-aaa-syntax/2350864"&gt;question on stackoverflow&lt;/a&gt; about &lt;a href="http://www.ayende.com/projects/rhino-mocks.aspx"&gt;Rhino Mocks&lt;/a&gt;. The issue in the question comes down to a &lt;b&gt;confusion between mocks and stubs&lt;/b&gt; as it often does. Stubs and mocks are both test doubles (i.e. a way to quickly replace a real class implementation by one that you have full control over) but they are set up in different ways.&lt;br /&gt;&lt;br /&gt;The distinction between mocks and stubs also exists in other mocking frameworks such as &lt;a href="http://www.jmock.org/"&gt;jMock&lt;/a&gt; for java. It is important to understand the difference if you are going to do &lt;a href="http://en.wikipedia.org/wiki/Test-driven_development"&gt;TDD&lt;/a&gt; with a mocking framework which supports both approaches.&lt;br /&gt;&lt;h3&gt;Mocks&lt;/h3&gt;A mock is a test double where you set up behavior by providing a sequence of expected method calls and the corresponding return values. You can think of a mock as an actor in a play who has to follow a script. The script specifies both expectations (i.e. what the other actors will say and when they'll say it) and behavior (i.e. how to respond).&lt;br /&gt;&lt;br /&gt;In practice, such a test script is recorded by making calls on the mock. After the behavior has been set up, you must explicitly switch a mock from record mode to replay mode. At the end of the test the expectations are verified. This is how it looks in Rhino Mocks:&lt;br /&gt;&lt;pre name="code" class="c#"&gt;[Test]&lt;br /&gt;public void Eat_uses_banana_provider()&lt;br /&gt;{&lt;br /&gt;   var mocks = new MockRepository();&lt;br /&gt;   var bananaProviderMock = mocks.DynamicMock&amp;lt;IBananaprovider&amp;gt;();&lt;br /&gt;   bananaProviderMock.GetBanana(); // record method call expectation&lt;br /&gt;   LastCall.Return(new Banana()); // tell mock what to do&lt;br /&gt;   bananaProviderMock.Replay(); // stop recording, go to replay mode&lt;br /&gt;   &lt;br /&gt;   // actual test&lt;br /&gt;   var monkey = new Monkey(bananaProviderMock);&lt;br /&gt;   monkey.Eat();&lt;br /&gt;&lt;br /&gt;   // verify the expectations that were set up earlier&lt;br /&gt;   bananaProviderMock.VerifyAll();&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;Actually, I lied: this is just one way how it can look. Unfortunately a lot of cruft has accumulated in Rhino Mocks (presumably for backwards compatibility) and it has become hard to tell which is the recommended syntax. &lt;br /&gt;&lt;h3&gt;Stubs&lt;/h3&gt;A stub is a test double that, unlike a mock, has no concept of a "test script". There are no separate record and replay phases: you just set up canned answers for method calls. Setting up this behavior will not create any expectations. Instead, the stub will record how it is used so that you can make assertions separately at the end of the test.  In Rhino Mocks it looks like this:&lt;br /&gt;&lt;pre name="code" class="c#"&gt;[Test]&lt;br /&gt;public void Eat_uses_banana_provider()&lt;br /&gt;{&lt;br /&gt;   var bananaProviderStub = MockRepository.GenerateStub&amp;lt;IBananaprovider&amp;gt;();&lt;br /&gt;   bananaProviderStub.Stub(x =&gt; x.GetBanana()).Return(new Banana());&lt;br /&gt;   &lt;br /&gt;   // actual test&lt;br /&gt;   var monkey = new Monkey(bananaProviderStub);&lt;br /&gt;   monkey.Eat();&lt;br /&gt;&lt;br /&gt;   // make assertion about how the stub was used&lt;br /&gt;   bananaProviderStub.AssertWasCalled(x =&gt; x.GetBanana());&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;h3&gt;Use stubs instead of mocks if possible&lt;/h3&gt;There are a few reasons to use stubs instead of mocks if possible:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Mocks are more powerful than stubs because they can do things like giving different answers for the first and second call. However, this will make your test &lt;b&gt;much harder to understand&lt;/b&gt;.&lt;/li&gt;&lt;li&gt;Mocks make it &lt;b&gt;hard to create tests that test only one thing&lt;/b&gt;. If the monkey class starts creating its own bananas after a change, then ideally only the &lt;i&gt;Eat_uses_banana_provider&lt;/i&gt; test fails. If &lt;i&gt;every&lt;/i&gt; monkey test starts failing you will waste time diagnosing the exact problem.&lt;/li&gt;&lt;li&gt;Mocks lead to &lt;b&gt;brittle tests&lt;/b&gt; because they expect a certain sequence of method calls. Unless you are explicitly testing that something happens in the expected order, this is rarely what you want.&lt;/li&gt;&lt;/ul&gt;If you must use mocks, use &lt;b&gt;dynamic&lt;/b&gt; mocks rather than strict mocks (Rhino Mocks offers both). Dynamic mocks will not throw exceptions when they receive an unexpected method call. They will return a default return value if necessary (e.g. 0 or null) and will keep on trucking.&lt;br /&gt;&lt;br /&gt;Because of the disadvantages, &lt;a href="http://code.google.com/p/moq/"&gt;Moq&lt;/a&gt; (a popular and simpler alternative to Rhino Mocks) doesn't even offer the record/replay approach.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-5317833679707044975?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/5317833679707044975/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=5317833679707044975' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5317833679707044975'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5317833679707044975'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/02/mocking-frameworks-stubs-vs-mocks.html' title='Mocking frameworks: stubs vs mocks'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-6685569704062889091</id><published>2010-02-22T03:15:00.000+01:00</published><updated>2010-02-22T03:15:14.935+01:00</updated><title type='text'>Creating a web feed for a file system directory</title><content type='html'>I just wrote a little python script which generates a &lt;a href="http://en.wikipedia.org/wiki/Web_feed"&gt;web feed&lt;/a&gt; for a folder with text files. When run, the script detects the 10 last added/changed files and outputs them as entries in a feed file. This makes it possible to easily create a web feed with just some shell scripting.&lt;br /&gt;&lt;br /&gt;To use the script:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;download it &lt;a href="http://dl.dropbox.com/u/119154/permalink/dir2feed.py"&gt;here&lt;/a&gt; and make it executable&lt;/li&gt;&lt;li&gt;edit the configuration values at the start of the script&lt;/li&gt;&lt;li&gt;execute the script regularly, e.g. from a cron job&lt;/li&gt;&lt;li&gt;if not generating the file directly on a web server, publish the feed file on the web (e.g. with &lt;a href="http://linux.die.net/man/1/scp"&gt;scp&lt;/a&gt; or just write it to your public &lt;a href="http://mindinthewater.blogspot.com/2010/01/setting-up-dropbox-on-headless-linux.html"&gt;dropbox&lt;/a&gt; folder)&lt;br /&gt;&lt;/ul&gt;I have used the &lt;a href="http://validator.w3.org/feed/"&gt;W3C feed validation service&lt;/a&gt; to check that the resulting file is a valid &lt;a href="http://www.ietf.org/rfc/rfc4287.txt"&gt;atom syndication&lt;/a&gt; feed. However, the configuration values are important for the validation so check that the generated feed still validates after configuring.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-6685569704062889091?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/6685569704062889091/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=6685569704062889091' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/6685569704062889091'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/6685569704062889091'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/02/creating-web-feed-for-file-system.html' title='Creating a web feed for a file system directory'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-3590388886940413330</id><published>2010-02-16T03:22:00.040+01:00</published><updated>2010-02-22T03:32:07.821+01:00</updated><title type='text'>Building a NAS, part 7: ZFS snapshots, scrubbing and error reporting</title><content type='html'>Setting up a ZFS pool with redundancy can only protect you against disk failures. To protect yourself against accidental deletions or modifications of files, you can use &lt;b&gt;snapshots&lt;/b&gt;. You also need to explicitly start a ZFS &lt;b&gt;data scrub&lt;/b&gt; at regular intervals to make sure that any checksum failures are repaired. Such things are best automated, but you might still want to receive &lt;b&gt;reports&lt;/b&gt; so that you can keep an eye on things.&lt;br /&gt;&lt;h4&gt;Automated snapshots&lt;/h4&gt;Setting up automated snapshots for ZFS-FUSE on debian is surprisingly easy. Drop this script in &lt;tt&gt;/etc/cron.daily/&lt;/tt&gt;:&lt;br /&gt;&lt;blockquote&gt;#!/bin/bash&lt;br /&gt;zfs snapshot mypool/myfilesystem@`date +%Y.%m.%d:%H.%M`.auto&lt;br /&gt;&lt;/blockquote&gt;This will automatically create daily snapshots with a name like &lt;tt&gt;2010.02.02:06.25.auto&lt;/tt&gt;. Note that this will complicate things if you need to delete stuff to make room. As long as there is a snapshot referencing a file, it will continue to take space in the pool. Daily snapshots work best for a grow-only archive where you rarely need to delete something.&lt;br /&gt;&lt;br /&gt;A word of warning: the scripts in &lt;tt&gt;/etc/cron.daily&lt;/tt&gt; are only executed if they are executable and have no dots in their name. See &lt;tt&gt;man run-parts&lt;/tt&gt; for more details. Test with &lt;tt&gt;/etc/cron.hourly&lt;/tt&gt; to verify that everything works, then move the script to &lt;tt&gt;/etc/cron.daily&lt;/tt&gt;.&lt;br /&gt;&lt;h4&gt;Automated scrubbing&lt;/h4&gt;A ZFS pool can repair its checksum errors (if there is redundant storage) while still remaining on-line. This is called a &lt;b&gt;scrub&lt;/b&gt;. The recommended scrub interval for consumer grade disks is one week. Drop this script in &lt;tt&gt;/etc/cron.weekly&lt;/tt&gt;:&lt;br /&gt;&lt;blockquote&gt;#!/bin/bash&lt;br /&gt;zpool scrub mypool&lt;br /&gt;&lt;/blockquote&gt;&lt;h4&gt;Web feed reporting&lt;/h4&gt;A report of the scrub progress or the results of the last scrub can be shown with the &lt;tt&gt;zpool status&lt;/tt&gt; command. A list of all file systems and snapshots (including some useful statistics) can be shown with the &lt;tt&gt;zfs list -t all&lt;/tt&gt; command. To automate the reporting, I use this script in a cron job:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;#!/bin/bash&lt;br /&gt;reportfile=/root/poolreports/`date +%Y.%m.%d:%H.%M`.txt&lt;br /&gt;date &gt; ${reportfile}&lt;br /&gt;zpool status nas-pool 2&gt;&amp;1 &gt;&gt; ${reportfile}&lt;br /&gt;zfs list -t all 2&gt;&amp;1 &gt;&gt; ${reportfile}&lt;br /&gt;&lt;/blockquote&gt;I then generate a web feed for the &lt;tt&gt;/root/poolreports/&lt;/tt&gt; folder as I explained in my &lt;a href="http://mindinthewater.blogspot.com/2010/02/creating-web-feed-for-file-system.html"&gt;previous post&lt;/a&gt; and follow the feed with &lt;a href="http://www.google.com/reader"&gt;google reader&lt;/a&gt;.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-3590388886940413330?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/3590388886940413330/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=3590388886940413330' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/3590388886940413330'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/3590388886940413330'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/02/building-nas-part-7-zfs-snapshots.html' title='Building a NAS, part 7: ZFS snapshots, scrubbing and error reporting'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-7117971592635857992</id><published>2010-02-11T19:19:00.002+01:00</published><updated>2010-02-11T23:59:15.117+01:00</updated><title type='text'>Debugging: Why is the Tick event of my WinForms timer no longer raised</title><content type='html'>I was debugging an issue at work today were a &lt;a href="http://msdn.microsoft.com/en-us/library/system.windows.forms.timer.aspx"&gt;WinForms Timer&lt;/a&gt; object was apparently no longer firing Tick events as it was supposed to.&lt;br /&gt;&lt;br /&gt;It turned out that the timer was inadvertently being used by a &lt;a href="http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx"&gt;BackgroundWorker&lt;/a&gt; thread. Like most classes, winforms timers are not thread safe so any behavior guarantees are out the window as soon as you start accessing them from different threads without synchronization measures. &lt;br /&gt;&lt;br /&gt;Worse, winforms timers interact with the main application thread directly so in this case it is not possible to put such synchronization measures in place. I like to call such classes &lt;b&gt;thread-hostile&lt;/b&gt;. Another sure way to create thread-hostile code is to use global variables; we have our fair share of such problems in our legacy code base.&lt;br /&gt;&lt;br /&gt;The following sample reproduces the timer problem by accessing a timer from a ThreadPool worker thread; the timer will only be fired once instead of indefinitely as you might expect:&lt;br /&gt;&lt;br /&gt;&lt;pre name="code" class="c#"&gt;public partial class Form1 : Form&lt;br /&gt;   {&lt;br /&gt;      private System.Windows.Forms.Timer fTimer;&lt;br /&gt;&lt;br /&gt;      public Form1()&lt;br /&gt;      {&lt;br /&gt;         InitializeComponent();&lt;br /&gt;         fTimer = new System.Windows.Forms.Timer();&lt;br /&gt;         fTimer.Interval = 1000;&lt;br /&gt;         fTimer.Tick += HandleTimerTick;&lt;br /&gt;         fTimer.Start();&lt;br /&gt;      }&lt;br /&gt;&lt;br /&gt;      private void HandleTimerTick(object sender, EventArgs args)&lt;br /&gt;      {&lt;br /&gt;         // sabotage timer by stopping/starting it from another thread&lt;br /&gt;         ThreadPool.QueueUserWorkItem(&lt;br /&gt;            delegate&lt;br /&gt;            {&lt;br /&gt;               fTimer.Stop();&lt;br /&gt;               fTimer.Start();&lt;br /&gt;            } );&lt;br /&gt;&lt;br /&gt;         MessageBox.Show("Timer tick");&lt;br /&gt;      }&lt;br /&gt;   }&lt;/pre&gt;&lt;br /&gt;In our case, the worker thread touched the timer in a much more indirect way: the background task was using a service which leaked side effects into the rest of the system via events, resulting in inadvertent multi-threaded access all over the place.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Conclusion:&lt;/b&gt; if you are going to do multi-threading, make sure threads are well-isolated and only communicate with the rest of the system via well defined synchronization points.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Wishlist item:&lt;/b&gt; wouldn't it be nice if you had to explicitly mark methods before they could be used by multiple threads? The C# compiler could then generate optional checks that make your code fail fast when there is an accidental "threading leak". It wouldn't surprise me if the language will actually grow such a debugging feature in the future; &lt;a href="http://blogs.msdn.com/pfxteam/"&gt;multi-threaded .NET programming is on the rise&lt;/a&gt; yet still wildly dangerous.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-7117971592635857992?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/7117971592635857992/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=7117971592635857992' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7117971592635857992'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7117971592635857992'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/02/debugging-why-is-tick-event-of-my.html' title='Debugging: Why is the Tick event of my WinForms timer no longer raised'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-2776773211807618297</id><published>2010-02-04T01:47:00.005+01:00</published><updated>2010-02-08T02:17:52.845+01:00</updated><title type='text'>OpenID: great standard, many poor implementations</title><content type='html'>&lt;p&gt;I was browsing slashdot the other day, and noticed an interesting story that I wanted to upvote, which requires logging in. Interestingly, there's an &lt;b&gt;openid option&lt;/b&gt;:&lt;br /&gt;&lt;/p&gt;&lt;img src="http://dl.dropbox.com/u/119154/permalink/slashdot.login.png"/&gt;&lt;br /&gt;&lt;p&gt;&lt;a href="http://en.wikipedia.org/wiki/Openid"&gt;OpenID&lt;/a&gt; is a standard that allows you to reuse a single identity on different websites (or any other service that requires an identity). Chances are you already have an OpenID. For example, if you have a google account, then you can use the URL &lt;b&gt;http://www.google.com/profiles/yourusername&lt;/b&gt; as an OpenID. There are many more OpenID providers like Yahoo, MyOpenID, AOL, LiveJournal, Wordpress, Blogger, Versign, etcetera.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Currently I have 130 user accounts on the web that I have bothered to keep track of. The idea of OpenID is that you no longer have to create hundreds of accounts, each with their own user name and password (or worse, the &lt;i&gt;same&lt;/i&gt; password). You just enter your OpenID, and the OpenID provider takes care of authenticating you.&lt;br /&gt;&lt;/p&gt;&lt;h4&gt;Stackoverflow gets it right&lt;/h4&gt;For an example of OpenID done right, try the &lt;a href="http://stackoverflow.com/users/login"&gt;stackoverflow login page&lt;/a&gt;. See how easy that was? No passwords, no confirmation mails, just reuse your existing identity by clicking the icon of your identity provider. As Steve Jobs would say, &lt;i&gt;isn't that wonderful&lt;/i&gt;?&lt;br /&gt;&lt;/p&gt;&lt;h4&gt;Slashdot gets it wrong&lt;/h4&gt;&lt;p&gt;Unfortunately, when you log in with your OpenID in slashdot you are greeted by this:&lt;br /&gt;&lt;/p&gt;&lt;img src="http://dl.dropbox.com/u/119154/permalink/slashdot.openid.png"/&gt;&lt;br /&gt;&lt;p&gt;In other words, you &lt;i&gt;still&lt;/i&gt; have to create a username and password specifically for slashdot. Worse, even if you do that you still cannot login with just your OpenID. What gives?&lt;br /&gt;&lt;/p&gt;&lt;h4&gt;Facebook gets it wrong&lt;/h4&gt;&lt;p&gt;You can go into your facebook &lt;i&gt;Settings - Account Settings - Linked Accounts - Change - Add Account&lt;/i&gt; and enter an OpenID there. If you then log out and try to log back in to test it, &lt;i&gt;there is no OpenID&lt;/i&gt; option on the login page. WTF? On a hunch, I then just retyped the facebook URL in my browser address bar and it looked like I was already logged in.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;A little more investigation shows that facebook relies on a cookie that links your browser to your OpenID, and tries to log you in transparently with that information. Since I have configured my browser to only keep cookies between browser sessions for a small white-list of websites, this doesn't work for me at all. Even if I add facebook to the white-list, I won't be able to use my OpenID to log in on other computers. &lt;b&gt;FAIL&lt;/b&gt;. I guess just putting a "Log in with OpenID" button on the login page would have been too easy.&lt;br /&gt;&lt;/p&gt;&lt;h4&gt;Dealing with lack of OpenID support&lt;/h4&gt;&lt;p&gt;OpenID support is growing, but the majority of web sites still don't support it or implement their support very poorly. Others only support OpenID as an identity provider and refuse to accept identities from other providers.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;To deal with all these sites that still require passwords, most people reuse the same password over and over again. This is terrible security. Any of the sites that you use could have a malicious admin that may like to sell username/password combos to the highest bidder. Or maybe the website admin isn't malicous, but the user account database might store passwords unhashed and could be compromised.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Personally I use the cross-platform &lt;a href="http://keepass.info/"&gt;KeePass&lt;/a&gt; application to maintain a personal &lt;b&gt;encrypted database of passwords&lt;/b&gt;. The database is protected by a single master password (or passphrase). I put mine in my &lt;a href="http://www.dropbox.com"&gt;dropbox&lt;/a&gt; folder, so I have access to my passwords on each PC I use. Even better, if you stick to version 1.x the database is compatible with &lt;a href="http://www.keepassmobile.com/"&gt;KeePassMobile&lt;/a&gt; so you can carry your passwords with you on your phone.&lt;br /&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-2776773211807618297?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/2776773211807618297/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=2776773211807618297' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2776773211807618297'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2776773211807618297'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/02/openid-great-standard-many-poor.html' title='OpenID: great standard, many poor implementations'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-2857733612828955926</id><published>2010-02-02T01:17:00.003+01:00</published><updated>2010-02-12T00:00:12.459+01:00</updated><title type='text'>MEF: GetExport, GetExportedValue methods compared to Import attribute</title><content type='html'>(Also &lt;a href="http://mef.codeplex.com/Thread/View.aspx?ThreadId=82796"&gt;posted on the MEF forum&lt;/a&gt;)&lt;br /&gt;&lt;br /&gt;While writing some automated composition tests today, I found out the hard way that GetExportedValue&amp;lt;Lazy&amp;lt;T&amp;gt;&amp;gt; doesn't do what you might think it does at first sight. For example, the following throws because it tries to get a exported value for the type Lazy&amp;lt;IFoo&amp;gt;, which is not an available part:&lt;br /&gt;&lt;pre name="code" class="c#"&gt;   public class Program&lt;br /&gt;   {&lt;br /&gt;      public static void Main(string[] args)&lt;br /&gt;      {&lt;br /&gt;         var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());&lt;br /&gt;         var container = new CompositionContainer(catalog);&lt;br /&gt;         var exports = container.GetExportedValue&amp;lt;Lazy&amp;lt;IFoo&amp;gt;&amp;gt;();&lt;br /&gt;         Console.WriteLine(exports.Count());&lt;br /&gt;      }&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   public interface IFoo&lt;br /&gt;   {&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   [Export(typeof(IFoo))]&lt;br /&gt;   public class Foo : IFoo&lt;br /&gt;   {&lt;br /&gt;   }&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;As it turns out, if you want to pull a lazy export from a container, you just have to call GetExport&amp;lt;T&amp;gt; or GetExport&amp;lt;T,TMetaData&amp;gt;. This is quite obvious with hindsight, but I just got so used to using the shorthand GetExportedValue&amp;lt;T&amp;gt; that I completely forgot about the existence of GetExport&amp;lt;T&amp;gt;.&lt;br /&gt;&lt;br /&gt;This then led me to wonder why the GetExport and GetExportedValue methods work differently from the Import attribute. With the Import attribute, MEF inspects the type you are trying to import and gives special treatment to Lazy&amp;lt;T&amp;gt;. Shouldn't there be an ImportLazy (and ImportManyLazy) attribute instead to make this intention explicit?&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-2857733612828955926?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/2857733612828955926/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=2857733612828955926' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2857733612828955926'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2857733612828955926'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/02/mef-getexport-getexportedvalue-methods.html' title='MEF: GetExport, GetExportedValue methods compared to Import attribute'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-5857593767361816554</id><published>2010-01-31T23:32:00.001+01:00</published><updated>2010-01-31T23:32:38.316+01:00</updated><title type='text'>On the emergence of ubiquitous computing</title><content type='html'>I just read &lt;a href="http://stevenf.tumblr.com/post/359224392/i-need-to-talk-to-you-about-computers-ive-been"&gt;this post&lt;/a&gt; about how the iPhone and new iPad herald the era of "new world computing":&lt;br /&gt;&lt;blockquote&gt;&lt;i&gt;In the New World, computers are task-centric. We are reading email, browsing the web, playing a game, but not all at once. Applications are sandboxed, then moats dug around the sandboxes, and then barbed wire placed around the moats. As a direct result, New World computers do not need virus scanners, their batteries last longer, and they rarely crash, but their users have lost a degree of freedom. New World computers have unprecedented ease of use, and benefit from decades of research into human-computer interaction. They are immediately understandable, fast, stable, and laser-focused on the 80% of the famous 80/20 rule.&lt;br /&gt;&lt;/i&gt;&lt;/blockquote&gt;&lt;br /&gt;It is an interesting post, but I don't believe that the lack of multi-tasking and other freedoms is a necessity for "new world computing". If you can make a slick UI for switching between tasks, then you can also make a slick UI for switching between tasks that continue to run in the background. &lt;br /&gt;&lt;br /&gt;These limitations are just engineering trade-offs that had to be made to give us an early peek at &lt;a href="http://en.wikipedia.org/wiki/Ubiquitous_computing"&gt;ubiquitous computing&lt;/a&gt; (which is by the way the real term for "new world computing" and was already a research topic long before I went to college). I seriously doubt the next generation of these devices will have the same limitations. &lt;br /&gt;&lt;br /&gt;You can wave your hands and talk about how &lt;i&gt;it's all task oriented now&lt;/i&gt; all you want, but in the end multi-tasking is a necessity even if only for running a chat client 24/7. And that's exactly what I do with my old and clunky N95 smartphone.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-5857593767361816554?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/5857593767361816554/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=5857593767361816554' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5857593767361816554'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5857593767361816554'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/01/on-emergence-of-ubiquitous-computing.html' title='On the emergence of ubiquitous computing'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-6680762756439459056</id><published>2010-01-31T02:13:00.007+01:00</published><updated>2010-01-31T03:24:45.021+01:00</updated><title type='text'>Setting up dropbox on a headless linux system</title><content type='html'>&lt;p&gt;I have been using &lt;a href="https://www.dropbox.com/"&gt;dropbox&lt;/a&gt; for a while to synchronize files between different computers. It has some pretty impressive bullet points:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;b&gt;Seamless syncing&lt;/b&gt;. You just put files in the dropbox folder, and they are automatically synchronized to your other computers. No firewall issues. In fact, the only problem I have is at work where dropbox is explicitly blocked. &gt;:-(&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Easy &lt;b&gt;file sharing&lt;/b&gt; over the internet. Just put a file in your public folder, right click, copy public link. You can even host a website on dropbox this way.&lt;/li&gt;&lt;li&gt;&lt;b&gt;Cross-platform&lt;/b&gt;. It works on linux, windows, OS X and even iPhone.&lt;/li&gt;&lt;li&gt;You can access the &lt;b&gt;revision history&lt;/b&gt; of your files so it works pretty well as an on-line backup service, even if you delete files by accident.&lt;/li&gt;&lt;li&gt;It's completely &lt;b&gt;free&lt;/b&gt; if you don't need more than 2GB storage and 30 days of revision history.&lt;/li&gt;&lt;/ul&gt;&lt;/p&gt;&lt;p&gt;I have set up dropbox on my NAS so that I can synchronize my dropbox to a &lt;a href="http://en.wikipedia.org/wiki/ZFS"&gt;ZFS file system&lt;/a&gt;. This way I can combine the advantages of dropbox with the advantages of my NAS:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;I get to keep snapshots indefinitely, with disk space being my only limitation.&lt;/li&gt;&lt;li&gt;I protect my data even if the dropbox service fails disastrously, e.g. because of security breach. Think file deletions being synced to all your computers.&lt;br /&gt;&lt;li&gt;I can free space on my dropbox account by moving files on the NAS out of the dropbox folder, yet still keep them safe through my NAS snapshot+backup policy.&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;/p&gt;&lt;p&gt;Dropbox is targetted at GUI environments, but can still be installed on a headless linux system as described on this &lt;a href="http://wiki.dropbox.com/TipsAndTricks/TextBasedLinuxInstall"&gt;wiki page&lt;/a&gt;. However, the wiki page did not describe how to change the dropbox folder. I needed this to &lt;b&gt;point dropbox to a folder on my NAS storage pool&lt;/b&gt;. It took some minor reverse engineering of the dropbox settings file, but I successfully created a &lt;a href="http://dl.dropbox.com/u/119154/permalink/dropboxdir.py"&gt;script&lt;/a&gt; to do exactly that. I've also added a link and instructions on the wiki on how to use it. &lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-6680762756439459056?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/6680762756439459056/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=6680762756439459056' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/6680762756439459056'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/6680762756439459056'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/01/setting-up-dropbox-on-headless-linux.html' title='Setting up dropbox on a headless linux system'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-5133897429466550942</id><published>2010-01-27T01:07:00.048+01:00</published><updated>2010-02-11T23:58:20.515+01:00</updated><title type='text'>Using MEF for classes which take configuration values</title><content type='html'>We're using the &lt;a href="http://mef.codeplex.com"&gt;Managed Extensibility Framework&lt;/a&gt; (part of the upcoming .NET 4.0) at work for a new project.&lt;br /&gt;&lt;br /&gt;I have a few pre-MEF classes which take strings, integers other primitive data types in their constructor. For example, consider the following C# class which tracks recently used resources (e.g. the last files opened by the application) by saving them in a file:&lt;br /&gt;&lt;br /&gt;&lt;pre name="code" class="c#"&gt;[Export(typeof(IRecentlyUsedTracker))]&lt;br /&gt;public class RecentlyUsedTracker : IRecentlyUsedTracker&lt;br /&gt;{&lt;br /&gt;   private readonly string file;&lt;br /&gt;   private readonly int maxItems;&lt;br /&gt;&lt;br /&gt;   // constructor&lt;br /&gt;   public RecentlyUsedTracker(string recentlyUsedFile, int maxItems)&lt;br /&gt;   {&lt;br /&gt;      this.file = recentlyUsedFile;&lt;br /&gt;      this.maxItems = maxItems;&lt;br /&gt;   }&lt;br /&gt;&lt;br /&gt;   // Marks the given resource as recently used.&lt;br /&gt;   public void Touch(string resource)&lt;br /&gt;   {&lt;br /&gt;      ...&lt;br /&gt;   }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;The above export doesn't work, because MEF cannot instantiate this class. Obviously it cannot know which string and integer to use as arguments for the constructor.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;You can still do it by adding [Import("somename")] attributes to the constructor declaration like this:&lt;br /&gt;&lt;pre name="code" class="c#"&gt;    // constructor&lt;br /&gt;    public RecentlyUsedTracker(&lt;br /&gt;       [Import("RecentlyUsedTracker.File")] string recentlyUsedFile,&lt;br /&gt;       [Import("RecentlyUsedTracker.MaxItems")] int maxItems)&lt;br /&gt;&lt;/pre&gt;&lt;strike&gt;However, that makes it much more complex to set up the container. Each configuration value has to be explicitly added to the MEF container with &lt;a href="http://msdn.microsoft.com/en-us/library/dd986608%28VS.100%29.aspx"&gt;ComposeExportedValue&lt;/a&gt; as shown below. Blergh!&lt;/strike&gt; (correction: see update below!)&lt;br /&gt;&lt;pre name="code" class="c#"&gt;var catalog = ... some catalog ...&lt;br /&gt;var container = new CompositionContainer(catalog);&lt;br /&gt;container.ComposeExportedValue&amp;lt;string&amp;gt;("RecentlyUsedTracker.File", @"c:\recentlyused.txt");&lt;br /&gt;container.ComposeExportedValue&amp;lt;int&amp;gt;("RecentlyUsedTracker.MaxItems", 5);&lt;br /&gt;&lt;/pre&gt;&lt;/p&gt;&lt;p&gt;My next idea was then to do something like this for the constructor:&lt;br /&gt;&lt;pre name="code" class="c#"&gt;   public RecentlyUsedTracker([Import] IConfigurationProvider configurationProvider)&lt;br /&gt;   {&lt;br /&gt;      this.recentlyUsedFile = configurationFile.GetValue&amp;lt;string&amp;gt;("recentlyUsedTracker.File");&lt;br /&gt;      this.maxItems = configurationProvider.GetValue&amp;lt;int&amp;gt;("recentlyUsedTracker.maxItems");&lt;br /&gt;   }&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;I'm a bit worried about the fact that I'm importing the configurationProvider object only to use it briefly in the constructor. It's also annoying that I need to mock this service in my unit tests, instead of just passing a value. I've &lt;a href="http://mef.codeplex.com/Thread/View.aspx?ThreadId=82114"&gt;asked on the MEF forum&lt;/a&gt; if there is a better way.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;Update:&lt;/b&gt; turns out there &lt;i&gt;is&lt;/i&gt; &lt;a href="http://codebetter.com/blogs/glenn.block/archive/2010/01/27/how-do-i-expose-configuration-information-through-mef.aspx"&gt;a better way&lt;/a&gt;. My first attempt (adding attributes to constructor arguments) is just fine. It's just explicitly adding configuration values to the container that was the bad idea. As Glenn Block suggested, you can just export the configuration values via properties like this:&lt;br /&gt;&lt;pre name="code" class="c#"&gt;public class RecentlyUsedTrackerConfiguration&lt;br /&gt;{&lt;br /&gt;  public RecentlyUsedTrackerConfiguration()&lt;br /&gt;  {&lt;br /&gt;     //set values here&lt;br /&gt;  }&lt;br /&gt;&lt;br /&gt;  [Export("RecentlyUsedTracker.File")]&lt;br /&gt;  public string File {get;set;}&lt;br /&gt;&lt;br /&gt;  [Export("RecentlyUsedTracker.MaxItems")]&lt;br /&gt;  public int MaxItems {get;set;}&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;/pre&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-5133897429466550942?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/5133897429466550942/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=5133897429466550942' title='4 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5133897429466550942'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5133897429466550942'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/01/using-mef-with-classes-which-take.html' title='Using MEF for classes which take configuration values'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>4</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-1334000203169504984</id><published>2010-01-26T03:27:00.004+01:00</published><updated>2010-02-12T00:06:09.500+01:00</updated><title type='text'>Building a NAS, part 6: testing ZFS checksumming</title><content type='html'>&lt;p&gt;Let's take a look at how ZFS protects data. I plugged in a spare external disk, created two small 1GB partitions on it with &lt;tt&gt;fdisk&lt;/tt&gt;, and set up a ZFS pool for testing:&lt;br /&gt;&lt;blockquote&gt;fdisk /dev/sdc # set up two 1GB partitions&lt;br /&gt;zpool create testpool mirror /dev/sdc1 /dev/sdc2&lt;br /&gt;zfs create testpool/testfs&lt;br /&gt;&lt;/blockquote&gt;Note that this is just a test set-up. Normally you should definitely use two separate disks to get the full benefit of mirroring. Also, it doesn't really make sense to slice up disks into partitions.&lt;br /&gt;&lt;/p&gt;&lt;h4&gt;Smashing bits&lt;/h4&gt;Let's create a test file which fills the file system and make a note of the sha1 fingerprint:&lt;br /&gt;&lt;blockquote&gt;cd /testpool/testfs&lt;br /&gt;dd if=/dev/urandom of=testfile bs=1M count=920&lt;br /&gt;&lt;i&gt;# prints a sha1 fingerprint for the file&lt;/i&gt;&lt;br /&gt;sha1sum /testpool/testfs/testfile&lt;br /&gt;&lt;/blockquote&gt;Now comes the fun part. With a small (and very dangerous) python script, we can corrupt one of the devices by writing some junk data at regular intervals:&lt;br /&gt;&lt;blockquote&gt;&lt;pre&gt;#!/usr/bin/python2.5&lt;br /&gt;openedDevice = open('/dev/sdc1', 'w+b')&lt;br /&gt;interval = 10000000&lt;br /&gt;while (True):&lt;br /&gt;  openedDevice.seek(interval,1)&lt;br /&gt;  print str(openedDevice.tell())&lt;br /&gt;  openedDevice.write('corrupt')&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;When we reread the file after the corruption, ZFS will transparently pick the pieces of data on the healthy disks. Note that in this case the file cannot not be cached in memory because it is larger than the available system memory.&lt;br /&gt;&lt;blockquote&gt;/home/wim/corruption.py&lt;br /&gt;&lt;i&gt;# still prints the correct fingerprint!&lt;/i&gt;&lt;br /&gt;sha1sum /testpool/testfs/testfile&lt;br /&gt;&lt;/blockquote&gt;Strangely enough, running &lt;tt&gt;zpool status testpool&lt;/tt&gt; doesn't report any errors at this point. I have send &lt;a href="http://groups.google.com/group/zfs-fuse/browse_thread/thread/aa5e3b949978cdc4"&gt;a mail&lt;/a&gt; to the &lt;a href="http://groups.google.com/group/zfs-fuse"&gt;zfs-fuse mailing list&lt;/a&gt; to ask whether this is normal.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;To detect and fix the errors, we have to run this simple command:&lt;br /&gt;&lt;blockquote&gt;zpool scrub testpool&lt;br /&gt;&lt;i&gt;# shows progress and results of the scrub&lt;/i&gt;&lt;br /&gt;zpool status testpool&lt;br /&gt;&lt;/blockquote&gt;To protect against bit rot on consumer grade disks, the recommendation is to run a scrub once a week. In a future post I'll explore how to do that automatically, including some kind of reporting so that I know when a disk is in trouble.&lt;br /&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-1334000203169504984?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/1334000203169504984/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=1334000203169504984' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/1334000203169504984'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/1334000203169504984'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/01/building-nas-part-6-testing-zfs.html' title='Building a NAS, part 6: testing ZFS checksumming'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-5141516734694459562</id><published>2010-01-24T19:57:00.009+01:00</published><updated>2010-06-07T18:20:36.639+02:00</updated><title type='text'>Building a NAS, part 5: minimizing power consumption</title><content type='html'>&lt;p&gt;My plan is to let my NAS run 24/7 if the impact on my electricity bill is acceptable. To measure power consumption, I have purchased a &lt;a href="http://www.profile.eu/media/35306/Handleiding%20-%20energiemeter%20PCF%20605%20-%20EN.pdf"&gt;power consumption meter&lt;/a&gt; that you can plug in between a wall socket and some device. It is one of those tools that can provide hours of quality geek entertainment. So much devices to measure around the house, so little time! :-)&lt;br /&gt;&lt;/p&gt;&lt;p&gt;The following table shows the passive power consumption of my NAS after each round of power saving measures. With "passive", I mean that the NAS was not doing anything useful like reading from or writing to the ZFS file systems.&lt;br /&gt;&lt;table width="400" border="1"&gt;&lt;tr&gt;&lt;th&gt;Configuration&lt;/th&gt;&lt;th&gt;power consumption (watts)&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;initial&lt;/td&gt;&lt;td align="right"&gt;80&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;under-clocked CPU&lt;/td&gt;&lt;td align="right"&gt;69&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;removed AGP video card&lt;/td&gt;&lt;td align="right"&gt;59&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;removed PATA CD-ROM drive&lt;/td&gt;&lt;td align="right"&gt;57&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;removed unused eSATA RAID1 PCI card&lt;/td&gt;&lt;td align="right"&gt;55&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2 storage disks in standby&lt;/td&gt;&lt;td align="right"&gt;47&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/p&gt;&lt;h4&gt;Take it slow&lt;/h4&gt;&lt;p&gt;The Athlon CPU and/or the mother board in this box is apparently too old to support dynamic cpu frequency scaling. It also doesn't seem to support the AMD power saving mode which you can control with the &lt;a href="http://packages.debian.org/lenny/athcool"&gt;athcool package&lt;/a&gt;. Instead, I had to get down and dirty with the BIOS settings and set the CPU multiplier to the minimum value. According to &lt;tt&gt;cat /proc/cpuinfo&lt;/tt&gt;, this slowed down the CPU from 2GHz to 1GHz. &lt;b&gt;Profit: -11 watts&lt;/b&gt;.&lt;br /&gt;&lt;/p&gt;&lt;h4&gt;Bare necessities&lt;/h4&gt;&lt;p&gt;I had to connect a video card and a CD-ROM drive to install debian Lenny. Now that the server is running and connected to the network, I can throw those out again and manage the system remotely. Fortunately the BIOS supports booting without a video card. I also removed a PCI card for eSATA RAID1 which I initially thought I would need. &lt;b&gt;Profit: -14 watts&lt;/b&gt;&lt;br /&gt;&lt;/p&gt;&lt;h4&gt;Spin down those storage disks&lt;/h4&gt;&lt;p&gt;With the &lt;tt&gt;hdparm&lt;/tt&gt; command we can inspect the power state of a disk and configure stand-by mode:&lt;br /&gt;&lt;blockquote&gt;aptitude install hdparm&lt;br /&gt;hdparm -C /dev/sda &lt;i&gt;# prints "active/idle"&lt;/i&gt;&lt;br /&gt;&lt;i&gt;# now tell disk to go to stand-by whenever not used for 2min&lt;br /&gt;# note the very strange S-value to time mapping; consult man hdparm!&lt;/i&gt;&lt;br /&gt;hdparm -S 25 /dev/sda &lt;br /&gt;sleep 120&lt;br /&gt;hdparm -C /dev/sda &lt;i&gt;# should print "stand-by"&lt;/i&gt;&lt;br /&gt;&lt;/blockquote&gt;To make the power saving configuration permanent, I added this to &lt;tt&gt;/etc/hdparm.conf&lt;/tt&gt;. Note the use of &lt;tt&gt;/dev/disk/by-id&lt;/tt&gt; to keep the settings correct even if we start changing the NAS hardware:&lt;br /&gt;&lt;blockquote&gt;/dev/disk/by-id/scsi-SATA_ST3500418AS_9VM7RWGV {&lt;br /&gt;spindown_time = 25&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;/dev/disk/by-id/scsi-SATA_ST3500418AS_9VM7SHA5 {&lt;br /&gt;spindown_time = 25&lt;br /&gt;}&lt;br /&gt;&lt;/blockquote&gt;Profit: &lt;b&gt;-8 watts&lt;/b&gt;&lt;br /&gt;&lt;p&gt;&lt;h4&gt;Leave the system disk alone&lt;/h4&gt;&lt;p&gt;It is much harder to get power savings for the system disk, because it is used all the time for logging and by a bunch of daemons. Trying to put this disk in stand-by will just cause it to frequently spin down and up again.&lt;br /&gt;&lt;/p&gt;Instead, I'm using a disk recovered from a dead laptop as the system drive. This requires a &lt;a href="http://www.dealextreme.com/details.dx/sku.2246"&gt;cheap 2.5" to 3.5" IDE converter cable&lt;/a&gt;. Such a drive is already extremely efficient; the potential savings of putting it in stand-by are negligible (~1 watt).&lt;br /&gt;&lt;/p&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;&lt;h4&gt;Cost before and after&lt;/h4&gt;&lt;p&gt;( &lt;i&gt;update: corrected kwh cost, my original estimate was about 50% of actual cost because I based it solely on Electrabel's power generation cost, and forgot the distribution costs on my bill - thank you Peter!&lt;/i&gt;)&lt;br /&gt;&lt;/p&gt;&lt;p&gt;At 80 watt, energy consumption per year was 80 watt * (24 * 365) hours = 700.8 kwh. At an average 0.16 euro/kwh that will cost &lt;b&gt;112 euros&lt;/b&gt;. I have reduced that to &lt;b&gt;66 euros&lt;/b&gt;. This goes to show that &lt;b&gt;a 20 euro energy consumption meter can yield a return on investment rather fast&lt;/b&gt;.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Another lesson I'll be remembering from these calculations: for my current contract, I can estimate the yearly cost for the continuous consumption of a device as &lt;b&gt;1.4 euro per watt&lt;/b&gt;.&lt;br /&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-5141516734694459562?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/5141516734694459562/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=5141516734694459562' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5141516734694459562'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5141516734694459562'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/01/building-nas-part-5-minimizing-power.html' title='Building a NAS, part 5: minimizing power consumption'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-7859038773374015462</id><published>2010-01-23T22:35:00.010+01:00</published><updated>2010-10-24T14:23:01.786+02:00</updated><title type='text'>Building a NAS, part 4: ZFS on linux with FUSE</title><content type='html'>&lt;p&gt;I decided to use the &lt;a href="http://en.wikipedia.org/wiki/ZFS"&gt;ZFS file system&lt;/a&gt; for my NAS. Although licensing issues prevent it from being ported to the linux kernel, there is a &lt;a href="http://zfs-fuse.net/"&gt;ZFS-FUSE project&lt;/a&gt; which has ported ZFS to run in userspace via &lt;a href="http://fuse.sourceforge.net/"&gt;FUSE&lt;/a&gt;.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;ZFS is a mature file system (and tool set) which does device pooling, redundant storage, checksumming, snapshots and copy-on-write clones. It also has a very cool &lt;a href="http://blogs.sun.com/bonwick/entry/zfs_dedup"&gt;deduplication&lt;/a&gt; feature where you can configure the file system to look for identical chunks of data and store those only once. Nice!&lt;br /&gt;&lt;/p&gt;&lt;h4&gt;Getting ZFS-FUSE on Debian Lenny&lt;/h4&gt;&lt;p&gt;We'll install some tools, compile and manually start the &lt;tt&gt;zfs-fuse&lt;/tt&gt; daemon. Note that I use the latest source from the "official" repository here, not the last stable release.&lt;br /&gt;&lt;blockquote&gt;aptitude install git-core libaio-dev libattr1-dev libacl1-dev libz-dev libfuse-dev libfuse2 scons libssl-dev&lt;br /&gt;git clone http://git.zfs-fuse.net/official zfs-official&lt;br /&gt;cd zfs-official/src&lt;br /&gt;scons&lt;br /&gt;scons install&lt;br /&gt;&lt;/blockquote&gt;&lt;/p&gt;At the time of writing, the "scons install" command doesn't seem to install the debian init script. Also, the debian init script which is part of the source has a small error. We'll take care of that manually:&lt;br /&gt;&lt;blockquote&gt;cd ../debian&lt;br /&gt;nano zfs-fuse.init&lt;br /&gt;&lt;i&gt;# fix the line "DAEMON=/usr/sbin/zfs-fuse"&lt;br /&gt;# it should be "DAEMON=/usr/local/sbin/zfs-fuse"&lt;/i&gt;&lt;br /&gt;su&lt;br /&gt;cp zfs-fuse.default /etc/default/zfs-fuse&lt;br /&gt;cp zfs-fuse.init /etc/init.d/zfs-fuse&lt;br /&gt;chmod +x /etc/init.d/zfs-fuse&lt;br /&gt;aptitude install sysv-rc-conf&lt;br /&gt;sysv-rc-conf&lt;br /&gt;&lt;i&gt;# use arrows to scroll down to zfs-fuse&lt;br /&gt;# use arrows and space to enable run levels 2,3,4,5&lt;br /&gt;# use q to quit&lt;/i&gt;&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;&lt;h4&gt;Setting up a ZFS storage pool and file systems&lt;/h4&gt;&lt;p&gt;Currently I have two new 500GB disks available for storage. My first plan was to split each disk in two partitions to build a "safe" storage pool (mirrored over two partitions) and a "bulk" storage pool (no redundancy, striped over two partitions). However, a recurring theme in the &lt;a href="http://www.solarisinternals.com/wiki/index.php/ZFS_Best_Practices_Guide"&gt;ZFS Best Bractices Guide&lt;/a&gt; is that you should not slice up your disks if you can avoid it. Therefore, I'll keep things simple and just create one big 500GB pool of mirrored storage. &lt;br /&gt;&lt;blockquote&gt;&lt;i&gt;# start the zfs daemon&lt;/i&gt;&lt;br /&gt;zfs-fuse&lt;br /&gt;zpool create nas-pool mirror \&lt;br /&gt;/dev/disk/by-id/scsi-SATA_ST3500418AS_9VM7RWGV \&lt;br /&gt;/dev/disk/by-id/scsi-SATA_ST3500418AS_9VM7SHA5&lt;br /&gt;zpool status&lt;br /&gt;&lt;/blockquote&gt;I will however, still create two separate file systems in this pool for "archive" and "bulk" storage. This makes it easy to have different backup policies for each data set.&lt;br /&gt;&lt;blockquote&gt;zfs create nas-pool/archive&lt;br /&gt;zfs create nas-pool/bulk&lt;br /&gt;zfs list&lt;br /&gt;zfs mount -a&lt;br /&gt;&lt;/blockquote&gt;Because ZFS is designed to handle storage pools with potentially thousands or more file systems, you don't have to manually edit &lt;tt&gt;/etc/fstab&lt;/tt&gt; to set up mount points. The shown mount command will automatically mount all available ZFS file systems as &lt;tt&gt;/&lt;i&gt;pool-name&lt;/i&gt;/&lt;i&gt;file-system-name&lt;/i&gt;&lt;/tt&gt;. This is also what the init script does.&lt;br /&gt;&lt;/p&gt;&lt;h4&gt;Exposing the ZFS file systems on the network via samba&lt;/h4&gt;&lt;p&gt;First we'll set up a "nasusers" group which has read/write access to the ZFS file system:&lt;br /&gt;&lt;blockquote&gt;&lt;i&gt;# create nasusers group and add a user to it&lt;/i&gt;&lt;br /&gt;groupadd nasusers&lt;br /&gt;usermod -a -G nasusers wim&lt;br /&gt;&lt;br /&gt;&lt;i&gt;# give nasusers read/write access&lt;/i&gt;&lt;br /&gt;cd /nas-pool&lt;br /&gt;chmod 2770 archive&lt;br /&gt;chmod 2770 bulk&lt;br /&gt;chgrp nasusers archive&lt;br /&gt;chgrp nasusers bulk&lt;br /&gt;&lt;/blockquote&gt;Now give those users a samba password:&lt;br /&gt;&lt;blockquote&gt;smbpasswd -a wim&lt;br /&gt;&lt;/blockquote&gt;Add a section like this to &lt;tt&gt;/etc/samba/smb.conf&lt;/tt&gt; like this for each folder to expose:&lt;br /&gt;&lt;blockquote&gt;[archive]&lt;br /&gt;path=/nas-pool/archive&lt;br /&gt;browsable=yes&lt;br /&gt;writable=yes&lt;br /&gt;valid users= @nasusers&lt;br /&gt;&lt;/blockquote&gt;Finally, restart samba:&lt;br /&gt;&lt;blockquote&gt;/etc/init.d/samba restart&lt;br /&gt;&lt;/blockquote&gt;Now the ZFS file systems should be available on the network, and users can start copying there stuff in there. In a future post we'll explore how to leverage some of those advanced ZFS features.&lt;br /&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-7859038773374015462?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/7859038773374015462/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=7859038773374015462' title='5 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7859038773374015462'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7859038773374015462'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/01/building-nas-part-4-zfs-on-linux-with.html' title='Building a NAS, part 4: ZFS on linux with FUSE'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>5</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-3461697031968879057</id><published>2010-01-22T01:56:00.004+01:00</published><updated>2010-01-22T02:08:41.548+01:00</updated><title type='text'>Building a NAS, part 3: filesystem doubts</title><content type='html'>I had planned to demonstrate the creation and administration of a raid1 BTRFS filesystem in this post, but while playing around with BTRFS I ran into a few snags:&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;I was able to mount a normal BTRFS filesystem spanning two devices, but not one in raid1 mode. Then I discovered that I could only mount a raid1 BTRFS filesystem if I gave it a label.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;I saw some unexplained mount failures on a multi-device FS which disappeared after I mounted once via another device.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Though data is checksummed, I couldn't find a way to detect checksum failures other than reading all files and watching output from the kernel with &lt;tt&gt;dmesg&lt;/tt&gt;.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;I confirmed that in a raid1 setup, BTRFS will still find the good copy after the data on one device is corrupted. However, I couldn't find a way to reliably repair the corruption other than reading and rewriting all files.&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;These issues didn't exactly give me confidence in the maturity of BTRFS. I knew it wasn't production ready, but I hoped it was close. I'm now pretty sure that it is not.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Perhaps more importantly, I also realized that BTRFS is the &lt;a href="http://www.gnu.org/licenses/gpl.html"&gt;GPL&lt;/a&gt;-licensed answer to the more mature &lt;a href="http://en.wikipedia.org/wiki/ZFS"&gt;ZFS&lt;/a&gt;. ZFS is a &lt;a href="http://www.sun.com/software/solaris/"&gt;Solaris&lt;/a&gt; filesystem developed by Sun. It can't be ported to run in the linux kernel because of &lt;a href="http://en.wikipedia.org/wiki/ZFS#Linux"&gt;licensing issues&lt;/a&gt;, hence the need for BTRFS. However, BTRFS is mainly &lt;a href="http://oss.oracle.com/projects/btrfs/"&gt;sponsored by Oracle&lt;/a&gt;, and &lt;a href="http://www.reuters.com/article/idUSTRE60K1ZN20100121"&gt;Oracle is buying Sun&lt;/a&gt;. If Oracle's motivation to sponsor BTRFS was to counter Sun's open source efforts, then the Sun deal takes away that motivation. On the other hand, if their motivation was truly to get a next-generation filesystem in linux, then they might as well relicense ZFS under the GPL. Chris Mason gave this vague comment &lt;a href="http://www.linux-foundation.org/weblogs/amanda/2009/06/22/a-conversation-with-chris-mason-on-btrfs-the-next-generation-file-system-for-linux/"&gt;when asked about ZFS and the Sun deal:&lt;/a&gt;&lt;br /&gt;&lt;blockquote&gt;Chris: Sun has many interesting projects, and I’m looking forward to working with their R&amp;D teams.  We’re committed to continuing Btrfs development, and ZFS doesn’t change our long term plans in that area.&lt;br /&gt;&lt;/blockquote&gt;That's no rational explanation why Oracle would continue to support both projects, so I'm skeptical.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Meanwhile, that leaves my little build-a-NAS project stalled. I see these options:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;use BTRFS anyway, even though it's not yet mature and its future seems unsure&lt;/li&gt;&lt;li&gt;run OpenSolaris and ZFS&lt;/li&gt;&lt;li&gt;run the FUSE port of ZFS on linux, which dodges the licensing issue by running the FS in userspace (presumably at the cost of performance)&lt;/li&gt;&lt;li&gt;use &lt;a href="http://tldp.org/HOWTO/html_single/Software-RAID-HOWTO/"&gt;software raid&lt;/a&gt; + &lt;a href="http://en.wikipedia.org/wiki/Logical_Volume_Manager_%28Linux%29"&gt;LVM&lt;/a&gt; on linux&lt;/li&gt;&lt;/ul&gt;I'm not sure at all which direction to take.&lt;br /&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-3461697031968879057?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/3461697031968879057/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=3461697031968879057' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/3461697031968879057'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/3461697031968879057'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/01/building-nas-part-3-filesystem-doubts.html' title='Building a NAS, part 3: filesystem doubts'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-2194161845162492904</id><published>2010-01-17T15:51:00.004+01:00</published><updated>2010-01-31T03:22:06.433+01:00</updated><title type='text'>Building a NAS, part 2: getting BTRFS on Lenny</title><content type='html'>&lt;i&gt;&lt;b&gt;update:&lt;/b&gt; after discovering that BTRFS isn't as mature as I hoped, I switched to ZFS-FUSE. You might want to read &lt;a href="http://mindinthewater.blogspot.com/2010/01/building-nas-part-4-zfs-on-linux-with.html"&gt;my post on setting up ZFS&lt;/a&gt; instead.&lt;/i&gt;&lt;br /&gt;&lt;br /&gt;The debian "lenny" release comes with version 2.6.26-2 of the &lt;a href="http://www.kernel.org/"&gt;linux kernel&lt;/a&gt;. This kernel does not yet have support for BTRFS, so we'll download, compile and install the latest stable kernel release.&lt;br /&gt;&lt;blockquote&gt;&lt;pre&gt;&lt;i&gt;# install some required packages as root&lt;/i&gt;&lt;br /&gt;su&lt;br /&gt;aptitude install install bzip2 fakeroot kernel-package libncurses5-dev zlib1g-dev&lt;br /&gt;exit&lt;br /&gt;&lt;br /&gt;&lt;i&gt;# download and extract linux kernel&lt;/i&gt;&lt;br /&gt;wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.32.3.tar.bz2&lt;br /&gt;tar -xvjf linux-2.6.32.3.tar.bz2&lt;br /&gt;cd linux-2.6.32.3&lt;br /&gt;&lt;br /&gt;&lt;i&gt;# copy existing kernel configuration from /boot&lt;/i&gt;&lt;br /&gt;cp /boot/config-2.6.26-2-686 .config&lt;br /&gt;&lt;br /&gt;&lt;i&gt;# edit kernel configuration (navigate with arrows, toggle options with space)&lt;br /&gt;# - under "File Systems, enable "Btrfs filesystem (EXPERIMENTAL)"&lt;br /&gt;# - under "Virtualization", &lt;b&gt;disable&lt;/b&gt; "Linux hypervisor example code"&lt;br /&gt;#&lt;br /&gt;# You can also take this opportunity to optimize the kernel for your CPU&lt;br /&gt;# architecture under "Processor type and features" - "Processor family"&lt;br /&gt;# Examine the output of "cat /proc/cpuinfo" if you're not sure of your CPU.&lt;/i&gt;&lt;br /&gt;make menuconfig&lt;br /&gt;&lt;br /&gt;&lt;i&gt;# build kernel (this takes a while, especially on old machines)&lt;/i&gt;&lt;br /&gt;make-kpkg --rootcmd fakeroot --initrd linux-image linux-headers&lt;br /&gt;&lt;br /&gt;&lt;i&gt;# install new kernel packages and reboot&lt;/i&gt;&lt;br /&gt;cd ..&lt;br /&gt;su&lt;br /&gt;dpkg -i linux-image-2.6.32.3_2.6.32.3-10.00.Custom_i386.deb&lt;br /&gt;dpkg -i linux-headers-2.6.32.3_2.6.32.3-10.00.Custom_i386.deb&lt;br /&gt;reboot&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;&lt;/p&gt;&lt;p&gt;If everything went well, the system should boot up under the new kernel. If something goes wrong, you still have the option of booting under the old kernel by using the grub menu at startup.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Now we have a kernel with support for the btrfs filesystem, but still no userspace tools to use it. We'll download, compile and install the &lt;a href="http://btrfs.wiki.kernel.org/index.php/Btrfs_source_repositories"&gt;latest version of those tools&lt;/a&gt;:&lt;br /&gt;&lt;blockquote&gt;&lt;pre&gt;aptitude install git-core uuid-dev e2fslibs-dev libacl1-dev&lt;br /&gt;git clone git://git.kernel.org/pub/scm/linux/kernel/git/mason/btrfs-progs-unstable.git&lt;br /&gt;cd btrfs-progs-unstable&lt;br /&gt;make&lt;br /&gt;su&lt;br /&gt;make install&lt;br /&gt;exit&lt;br /&gt;&lt;/pre&gt;&lt;/blockquote&gt;&lt;/p&gt;&lt;br /&gt;Now we have &lt;tt&gt;mkfs.btrfs&lt;/tt&gt; to create a BTRFS file system, and some other tools to manage such a filesystem. We'll start playing around with those in the next post.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-2194161845162492904?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/2194161845162492904/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=2194161845162492904' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2194161845162492904'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2194161845162492904'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/01/building-nas-part-2-getting-btrfs-on.html' title='Building a NAS, part 2: getting BTRFS on Lenny'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-6296029528116823581</id><published>2010-01-17T04:50:00.006+01:00</published><updated>2010-01-17T14:18:26.575+01:00</updated><title type='text'>Building a NAS, part 1: installing Lenny and introducing BTRFS</title><content type='html'>&lt;p&gt;I'm building a &lt;a href="http://en.wikipedia.org/wiki/Network-attached_storage"&gt;NAS &lt;/a&gt; from spare parts. The basic system is an 8 year old PC. My operating system of choice is the latest stable release of &lt;a href="http://www.debian.org/"&gt;debian gnu/linux&lt;/a&gt;, codename &lt;a href="http://www.debian.org/releases/lenny/"&gt;lenny&lt;/a&gt;. I would like to use the (still experimental) &lt;a href="http://en.wikipedia.org/wiki/Btrfs"&gt;btrfs file system&lt;/a&gt; on this box for the following reasons which make it a good data-haven:&lt;ul&gt;&lt;li&gt;It can do &lt;a href="http://en.wikipedia.org/wiki/RAID#RAID_1"&gt;raid1&lt;/a&gt;-like mirroring of data over multiple devices. This makes it resilient against failed disks.&lt;/li&gt;&lt;li&gt;It can make copy-on-write mountable snapshots of a volume. By regularly making snapshots (e.g. from a cron-job) you can keep old versions of your data without wasting any space on identical copies.&lt;/li&gt;&lt;/ul&gt;&lt;/p&gt;&lt;p&gt;Of course, this still won't fully protect my data. There could be hardware or software errors that destroy the data on all disks. There could be circumstances that destroy all disks together, like fire or lightning. So I'll still have to make an off-site backup every now and then.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;To install lenny, I downloaded the 40MB &lt;a href="http://www.debian.org/CD/netinst/"&gt;businesscard CD image&lt;/a&gt;. Despite its small download size, this CD still has a user-friendly graphical installer. After letting the installer do its thing, I still did the following:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;ran "tasksel" to install "file server" related packages&lt;/li&gt;&lt;li&gt;ran "aptitude install openssh-server" to enable remote access&lt;/li&gt;&lt;/ul&gt;&lt;/p&gt;&lt;p&gt;At this point I have a basic linux system that I can SSH into. Unfortunately "lenny" does not come with btrfs support. We'll fix that in the next post...&lt;br /&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-6296029528116823581?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/6296029528116823581/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=6296029528116823581' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/6296029528116823581'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/6296029528116823581'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/01/building-nas-part-1.html' title='Building a NAS, part 1: installing Lenny and introducing BTRFS'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-7586596258578627354</id><published>2010-01-11T23:29:00.035+01:00</published><updated>2010-01-12T12:59:05.818+01:00</updated><title type='text'>Registratierechten te betalen bij het kopen van een woning in Vlaanderen</title><content type='html'>&lt;p&gt;&lt;i&gt;This post is in Dutch because it is about law and taxes in the Flemish Region of Belgium.&lt;/i&gt;&lt;br /&gt;&lt;/p&gt;&lt;p&gt;&lt;i&gt;Deze post hoort thuis in een serie over de aankoop van ons huis. Eerder in deze serie:&lt;br /&gt;&lt;a href="http://mindinthewater.blogspot.com/2010/01/beeindigen-van-een-huurovereenkomst.html"&gt;Beëindigen van een huurovereenkomst door de huurder in België&lt;/a&gt;&lt;/i&gt;&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Wanneer je in het Vlaamse gewest vastgoed koopt, moet je daarop registratierechten betalen. Deze rechten zijn vastgelegd door het &lt;a href="http://ccff02.minfin.fgov.be/KMWeb/document.do?method=view&amp;id=0a341335-fc7f-4b3a-8ee6-8e59a57dbc35"&gt;Wetboek der registratie-, hypotheek- en griffierechten&lt;/a&gt;. &lt;a href="http://ccff02.minfin.fgov.be/KMWeb/document.do?method=view&amp;nav=1&amp;id=80c4a04b-d6ed-4b34-957d-3da3cafdff6b"&gt;Artikel 44&lt;/a&gt; geeft aan dat de registratierechten &lt;span style="font-weight:bold;"&gt;10 procent&lt;/span&gt; bedragen:&lt;br /&gt;&lt;/p&gt;&lt;blockquote&gt;Het recht bedraagt 10 ten honderd voor de verkoop, de ruiling en iedere overeenkomst tot overdracht onder bezwarende titel van eigendom of vruchtgebruik van onroerende goederen.&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;&lt;h3&gt;Vermindering tot 5 procent&lt;/h3&gt;&lt;p&gt;Volgens &lt;a href="http://ccff02.minfin.fgov.be/KMWeb/document.do?method=view&amp;nav=1&amp;id=8f1f022f-fec4-46e3-a9b8-4f20fa13affa"&gt;artikel 53&lt;/a&gt; wordt dit verminderd tot 5% voor &lt;i&gt;kleine landeigendommen en bescheiden woningen&lt;/i&gt;. Dit is vastgoed waarvan het kadastraal inkomen lager is dan een zeker maximum, vastgelegd door een apart Koninklijk Besluit. (Ik vind spijtig genoeg geen link naar het KB.)&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Dit staat ook wel bekend als &lt;i&gt;klein beschrijf&lt;/i&gt;. Er zijn nog bijkomende criteria vastgelegd door artikels 54-61; zeer droge kost, en aangezien de woning die Elke en ik gaan kopen toch niet in aanmerking komt, ga ik hier niet verder op in gaan.&lt;br /&gt;&lt;/p&gt;&lt;br /&gt;&lt;h3&gt;Vermindering op het bedrag waarop die 5 of 10 procent berekend wordt&lt;/h3&gt;&lt;p&gt;De bovenstaande percentages worden berekend op de prijs overeengekomen tussen koper en verkoper. Volgens artikel 46bis kan echter een deel van dit bedrag vrijgesteld worden, en die vrijstellingen mag je dus van de prijs aftrekken voor je die 5 of 10 procent berekend.&lt;br /&gt;&lt;/p&gt;&lt;p&gt;Er wordt 15.000 euro vrijgesteld als je het vastgoed koopt als hoofdverblijfplaats:&lt;br /&gt;&lt;/p&gt;&lt;blockquote&gt;De heffingsgrondslag ten aanzien van de verkopingen, zoals bepaald in de artikelen 45 en 46, wordt verminderd met 15.000 euro in geval van zuivere aankoop van de geheelheid volle eigendom van een tot bewoning aangewend of bestemd onroerend goed door een of meer natuurlijke personen om er hun hoofdverblijfplaats te vestigen.&lt;br /&gt;&lt;/blockquote&gt;&lt;p&gt;Deze vrijstelling heeft nog extra voorwaarden: &lt;br /&gt;&lt;ul&gt;&lt;li&gt;je mag nog geen onroerend goed bezitten&lt;/li&gt;&lt;li&gt;je moet &lt;span style="font-weight:bold;"&gt;uitdrukkelijk vragen&lt;/span&gt; om deze korting te krijgen (!)&lt;/li&gt;&lt;li&gt;je moet er binnen de 2 jaar gaan wonen (5 jaar in het geval van bouwground)&lt;/li&gt;&lt;/ul&gt;&lt;/p&gt;&lt;p&gt;Als je in aanmerking komt voor bovenstaande vermindering, wordt er nog 10.000 euro extra vrijgesteld als je voor de woning een lening aangaat (of zelfs 20.000 euro indien je in aanmerking komt voor de vermindering tot 5 procent):&lt;br /&gt;&lt;/p&gt;&lt;blockquote&gt;Als met het oog op de financiering van een aankoop, vermeld in het eerste lid, een hypotheek wordt gevestigd op het aangekochte onroerend goed, wordt het bedrag van de vermindering van de heffingsgrondslag, vermeld in het eerste lid, verhoogd met hetzij 10.000 euro als op de aankoop het recht, vermeld in artikel 44, verschuldigd is, hetzij 20.000 euro als op de aankoop het recht, vermeld in artikel 53, verschuldigd is&lt;/blockquote&gt;&lt;p&gt;In ons geval komen we in aanmerking voor beide verminderingen, dus we mogen 25.000 euro van de verkoopprijs aftrekken voordat we de 10 procent registratierechten berekenen. Dat "bespaart" ons dus 2500 euro.&lt;br /&gt;&lt;/p&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-7586596258578627354?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/7586596258578627354/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=7586596258578627354' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7586596258578627354'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/7586596258578627354'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/01/registratierechten-te-betalen-bij-het.html' title='Registratierechten te betalen bij het kopen van een woning in Vlaanderen'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-933102376647534802</id><published>2010-01-10T17:27:00.035+01:00</published><updated>2010-01-12T00:57:15.585+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='wet'/><title type='text'>Beëindigen van een huurovereenkomst door de huurder in België</title><content type='html'>&lt;i&gt;This post is in Dutch because it is about Belgian law and I live in the Flemish region. As far as I know the referred Belgian law is not published in English, so I had to pick another language.&lt;/i&gt;&lt;br /&gt;&lt;br /&gt;Aangezien Elke en ik net een verkoopsovereenkomst voor een huis getekend hebben, hebben we de eerstvolgende maanden heel wat te doen. Een van die dingen is het opzeggen van de huur.&lt;br /&gt;&lt;br /&gt;Onze huurovereenkomst is een typisch contract van 9 jaar. Zo'n contract wordt ook wel "een 3-6-9" genoemd in de volksmond omdat de huurwet bepalingen bevat waarin sprake is van driejarige periodes. De spelregels voor het vroegtijdig beeindigen van zo een overeenkomst worden vastgelegd door de Belgische wetgeving in &lt;a href="http://www.ejustice.just.fgov.be/cgi_loi/change_lg.pl?language=nl&amp;la=N&amp;cn=1991022032&amp;table_name=wet"&gt;het Burgerlijk Wetboek, "Regels betreffende de huurovereenkomsten met betrekking tot de hoofdverblijfplaats van de huurder in het bijzonder"&lt;/a&gt; en het bijhorende &lt;a href="http://www.ejustice.just.fgov.be/cgi/api2.pl?lg=nl&amp;pd=2007-05-21&amp;numac=2007009485"&gt;Koninklijk besluit&lt;/a&gt;. Een langere maar meer leesbare interpretatie van deze wetteksten is te vinden in de &lt;a href="http://www.just.fgov.be/cgi_justice/publications/show_pdf.pl?id=21"&gt;brochure "De Huurwet" (10e editie juli 2008)&lt;/a&gt; van de Vlaamse overheid.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Opzeggingstermijn&lt;/h3&gt;&lt;br /&gt;Een contract van 9 jaar kan altijd door de huurder opgezegd worden, maar de opzeg moet wel minstens 3 maanden op voorhand gebeuren:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;&lt;br /&gt;Art. 3, § 5 De huurder kan de huurovereenkomst op ieder tijdstip beëindigen met inachtneming van een opzeggingstermijn van drie maanden.&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;&lt;br /&gt;Die termijn gaat in vanaf de eerste dag van de maand die volgt op de opzegging:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;&lt;br /&gt;Art. 3, § 9. In alle gevallen waarin een opzegging te allen tijde kan worden gedaan, neemt de opzeggingstermijn een aanvang de eerste dag van de maand die volgt op de maand tijdens welke de opzegging wordt gedaan&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;&lt;br /&gt;Vreemd genoeg staat er iets anders in het &lt;a href="http://www.ejustice.just.fgov.be/cgi/api2.pl?lg=nl&amp;pd=2007-05-21&amp;numac=2007009485"&gt;KB&lt;/a&gt;, bijlage "HUUROVEREENKOMSTEN VOOR WONINGEN GELEGEN IN HET VLAAMS GEWEST". Het lijkt erop dat de woorden "de maand die volgt" verdwenen zijn (benadrukking toegevoegd door mezelf):&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;&lt;br /&gt;In alle gevallen waarin de opzegging te allen tijde kan worden gedaan, neemt de opzeggingstermijn een aanvang de &lt;span style="font-weight:bold;"&gt;eerste dag van de maand tijdens welke de opzegging wordt gedaan.&lt;/span&gt;&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Opzeggingsvergoeding&lt;/h3&gt;&lt;br /&gt;Als de huurder het huurcontract opzegt tijdens de eerste 3 jaar, dan heeft de verhuurder recht op een opzeggingsvergoeding. Ook uit Art. 3, § 5:&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;&lt;br /&gt;Indien de huurder de huurovereenkomst evenwel beëindigt tijdens de eerste driejarige periode, heeft de verhuurder recht op een vergoeding. Die vergoeding is gelijk aan drie maanden, twee maanden of één maand huur naargelang de huurovereenkomst een einde neemt gedurende het eerste, het tweede of het derde jaar.&lt;br /&gt;&lt;/blockquote&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-933102376647534802?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/933102376647534802/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=933102376647534802' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/933102376647534802'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/933102376647534802'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2010/01/beeindigen-van-een-huurovereenkomst.html' title='Beëindigen van een huurovereenkomst door de huurder in België'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-1316267035558202750</id><published>2009-08-18T00:58:00.005+02:00</published><updated>2009-08-18T01:43:23.625+02:00</updated><title type='text'>Emo Queries</title><content type='html'>In a moment of frustration, I started to type the query "Why is it so hard to call .NET assemblies from java?" in the little search field of my browser.&lt;br /&gt;&lt;br /&gt;I normally don't type full questions for my searches so I was a bit surprised when these &lt;a href="http://labs.google.com/suggestfaq.html"&gt;google suggestions&lt;/a&gt; popped up:&lt;br /&gt;&lt;br /&gt;&lt;center&gt;&lt;img src="http://dl.getdropbox.com/u/119154/permalink/hard.png"/&gt;&lt;/center&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-1316267035558202750?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/1316267035558202750/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=1316267035558202750' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/1316267035558202750'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/1316267035558202750'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2009/08/emo-queries.html' title='Emo Queries'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-1150285391668012068</id><published>2008-11-18T23:12:00.014+01:00</published><updated>2011-02-25T19:52:42.277+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='dotNET'/><title type='text'>Executing visual studio unit tests without installing visual studio</title><content type='html'>&lt;b&gt;UPDATE&lt;/b&gt;: I've written a &lt;a href="http://mindinthewater.blogspot.com/2011/02/executing-visual-studio-2010-unit-tests.html"&gt;new post for Visual Studio 2010&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;The professional edition of visual studio 2008 has a nicely integrated unit testing framework. I suppose visual studio team server will run these for you whenever somebody checks in a source code change. Unfortunately, we don't have team server or the team edition of visual studio at work.&lt;br /&gt;&lt;br /&gt;This lead me to look for a way to execute these unit tests on our build server. "build server" may be a bit of an euphemism; it is little more than a virtual machine with a collection of python and batch scripts and it doesn't have visual studio installed. In case you are wondering, compiling C# projects and solutions from the command line with msbuild works fine without installing visual studio. I wanted a similar solution for the unit tests.&lt;br /&gt;&lt;br /&gt;It turns out that there is a command line utility called mstest.exe that can be used to run these tests. This utility comes with visual studio. It is not straightforward to get it working without installing visual studio. Here's what I did to get mstest.exe running on our build server:&lt;br /&gt;&lt;br /&gt;1) Created a \tools\mstest folder on the buildserver&lt;br /&gt;2) Copied %ProgramFiles%\Microsoft Visual Studio 9.0\Common7\IDE\mstest.exe&lt;br /&gt;3) Copied these files from %ProgramFiles%\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies&lt;br /&gt;&lt;blockquote&gt;&lt;br /&gt;Microsoft.VisualStudio.QualityTools.AgentObject.dll&lt;br /&gt;Microsoft.VisualStudio.QualityTools.CommandLine.dll&lt;br /&gt;Microsoft.VisualStudio.QualityTools.Common.dll&lt;br /&gt;Microsoft.VisualStudio.QualityTools.ControllerObject.dll&lt;br /&gt;Microsoft.VisualStudio.QualityTools.ExecutionCommon.dll&lt;br /&gt;Microsoft.VisualStudio.QualityTools.Tips.UnitTest.Adapter.dll&lt;br /&gt;Microsoft.VisualStudio.QualityTools.Tips.UnitTest.AssemblyResolver.dll&lt;br /&gt;Microsoft.VisualStudio.QualityTools.Tips.UnitTest.ObjectModel.dll&lt;br /&gt;Microsoft.VisualStudio.QualityTools.Tips.UnitTest.Tip.dll&lt;br /&gt;Microsoft.VisualStudio.QualityTools.TMI.dll&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;&lt;br /&gt;4) Copied these files from c:\windows\assembly\gac_msil subfolders. This requires you to use the command line as windows explorer hides the actual files there.&lt;br /&gt;&lt;blockquote&gt;&lt;br /&gt;Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll&lt;br /&gt;Microsoft.VisualStudio.QualityTools.Resource.dll&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;&lt;br /&gt;5) Added certain registry keys to the build server registry. You can save these in a .reg file and then simply double click it:&lt;br /&gt;&lt;blockquote&gt;&lt;br /&gt;Windows Registry Editor Version 5.00&lt;br /&gt;&lt;br /&gt;[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\EnterpriseTools\QualityTools\HostAdapters\VS IDE\SupportedTestTypes]&lt;br /&gt;"{13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b}"=""&lt;br /&gt;&lt;br /&gt;[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\EnterpriseTools\QualityTools\TestTypes]&lt;br /&gt;@="TestTypes"&lt;br /&gt;&lt;br /&gt;[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\EnterpriseTools\QualityTools\TestTypes\{13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b}]&lt;br /&gt;"ServiceType"="Microsoft.VisualStudio.TestTools.TestTypes.Unit.SUnitTestService, Microsoft.VisualStudio.QualityTools.Tips.UnitTest.Tip, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"&lt;br /&gt;"NameId"="#212"&lt;br /&gt;"SatelliteBasePath"="C:\\Program Files\\Microsoft Visual Studio 9.0\\Common7\\IDE\\PrivateAssemblies\\"&lt;br /&gt;"SatelliteDllName"="Microsoft.VisualStudio.QualityTools.Tips.TuipPackageUI.dll"&lt;br /&gt;"VsEditor"="{00000000-0000-0000-0000-000000000000}"&lt;br /&gt;"TipProvider"="Microsoft.VisualStudio.TestTools.TestTypes.Unit.UnitTestTip, Microsoft.VisualStudio.QualityTools.Tips.UnitTest.Tip, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"&lt;br /&gt;&lt;br /&gt;[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\EnterpriseTools\QualityTools\TestTypes\{13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b}\Extensions]&lt;br /&gt;".dll"=dword:000000d7&lt;br /&gt;".exe"=dword:000000d7&lt;br /&gt;&lt;br /&gt;[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\EnterpriseTools\QualityTools\TestTypes\{13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b}\SupportedHostAdapters]&lt;br /&gt;"Smart Device"="Smart Device Host Adapter"&lt;br /&gt;"ASP.NET"="ASP.NET Host Adapter"&lt;br /&gt;"VS IDE"=""&lt;br /&gt;&lt;br /&gt;[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\EnterpriseTools\QualityTools\TestTypes\{ec4800e8-40e5-4ab3-8510-b8bf29b1904d}]&lt;br /&gt;"ServiceType"="Microsoft.VisualStudio.TestTools.TestTypes.Ordered.SOrderedTestService, Microsoft.VisualStudio.QualityTools.Tips.OrderedTest.Tip, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"&lt;br /&gt;"NameId"="#313"&lt;br /&gt;"SatelliteBasePath"="C:\\Program Files\\Microsoft Visual Studio 9.0\\Common7\\IDE\\PrivateAssemblies\\"&lt;br /&gt;"SatelliteDllName"="Microsoft.VisualStudio.QualityTools.Tips.TuipPackageUI.dll"&lt;br /&gt;"VsEditor"="{700218d8-f6f1-4ec3-be76-a35f72260503}"&lt;br /&gt;"TipProvider"="Microsoft.VisualStudio.TestTools.TestTypes.Ordered.AutoSuiteTip, Microsoft.VisualStudio.QualityTools.Tips.OrderedTest.Tip, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"&lt;br /&gt;&lt;br /&gt;[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\EnterpriseTools\QualityTools\TestTypes\{ec4800e8-40e5-4ab3-8510-b8bf29b1904d}\Extensions]&lt;br /&gt;".orderedtest"=dword:0000013c&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;&lt;br /&gt;After that, you can invoke mstest.exe on the command line like this:&lt;br /&gt;&lt;blockquote&gt;&lt;br /&gt;mstest /noisolation /testcontainer:"path\to\testproject.dll"&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;&lt;br /&gt;mstest.exe will return an error code if any test fails.&lt;br /&gt;&lt;br /&gt;I have not figured out how to get things working without the /noisolation switch. I believe that any problems caused by this switch are rare. In any case it will only cause problems if there are serious issues with the code under test which will need to be fixed anyway.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-1150285391668012068?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/1150285391668012068/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=1150285391668012068' title='8 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/1150285391668012068'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/1150285391668012068'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2008/11/executing-visual-studio-unit-tests.html' title='Executing visual studio unit tests without installing visual studio'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>8</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-2986240838625312072</id><published>2008-05-21T22:39:00.006+02:00</published><updated>2008-05-21T23:19:49.669+02:00</updated><title type='text'>Lessons from IT projects: allowing NULL foreign keys in a database can be tricky</title><content type='html'>My previous "lesson learned" was a bit general, and perhaps not very useful. Migrating old data is hard, but if the problem wasn't hard to fix we wouldn't be payed to fix it now would we? Today I'll focus on a much more simple technical dilemma: whether to allow database foreign keys to be NULL.&lt;br /&gt;&lt;br /&gt;Consider a database table of contacts, a classic example. Each contact has a name, telephone number, email, country_id, etc. The country_id field for the contact is actually a foreign key pointing to a country record in the country table.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-weight:bold;"&gt;Consider now that often we only have a few pieces of contact information, e.g. only a name and email address. If we don't know the country, we can't set it. It may seem natural to declare the country_id column to allow for NULL values.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;It's a possibility, but it has consquences. What kind of consequences?&lt;br /&gt;&lt;br /&gt;Suppose we'd like to pull all contact data from the database and show it on the screen or in a report. We also want the country data in the same view. The natural query for this is&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;SELECT * FROM Contact, Country WHERE Contact.country_id = Country.id&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Clean and simple. And unfortunately, broken. Your report will silently omit all contacts where country_id is NULL! Needless to say, this can be a serious bug if you are generating a billing/bookkeeping report.&lt;br /&gt;&lt;br /&gt;To fix this, you'd have to use a "LEFT JOIN" operation:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;SELECT * FROM Contact LEFT JOIN Country ON Contact.country_id = Country.id&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;Or you could just forbid country_id from ever being NULL like this:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;CREATE TABLE [dbo].[Contact](&lt;br /&gt; [id] [int] IDENTITY(1,1) NOT NULL,&lt;br /&gt; [name] [nvarchar](999),&lt;br /&gt; [address] [nvarchar](999),&lt;br /&gt; [CNT_CountryID] [int] NOT NULL,&lt;br /&gt;        ... more create table goodness ...&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;Then of course, you also need to put a special "UNKNOWN" country in your Country table. This solution has the "magic value smell" a bit, but after debugging a few problems caused by NULL foreign keys, I very much prefer to use a special unknown country over NULL. It also has the advantage that your code will need to deal less with NULL values, which can be annoying in itself. Come to think of it, those name and address fields should also be NOT NULL, just use empty strings when you don't know the values!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-2986240838625312072?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/2986240838625312072/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=2986240838625312072' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2986240838625312072'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/2986240838625312072'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2008/05/lessons-from-it-projects-allowing-null.html' title='Lessons from IT projects: allowing NULL foreign keys in a database can be tricky'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-4329955436004163545</id><published>2008-05-20T17:06:00.002+02:00</published><updated>2008-05-20T17:47:43.904+02:00</updated><title type='text'>Lessons from IT projects: migrating existing data to a new system</title><content type='html'>I'm currently working on a redesign/reimplementation/integration of an administrative system that currently exists as a mix of 3 databases (with semantically vague and manually maintained links between them), two applications and lord knows how many excel spreadsheets. The goal is to have 1 database and 1 application, less room for mistakes, and drastically reduced administrative overhead.&lt;br /&gt;&lt;br /&gt;As I approach the deadline for this project, I often find myself thinking "If only I had known that ..." or "I should remember X next time".&lt;br /&gt;&lt;br /&gt;&lt;span style="font-weight:bold;"&gt;A first observation: Migrating old data to a new system is harder than you think&lt;/span&gt;. It forces you to follow stupid constraints for the new system, because otherwise the old data can't be expressed in the new system. Imagine trying to build a house in the middle of the forest without being allowed to cut down certain trees. You'll end up with alot of extra walls to build around the forbidden trees, and your shiny new house will start to look like a maze.&lt;br /&gt;&lt;br /&gt;If at all possible, go for a green-field scenario where the data can be left behind or is migrated gradually/manually by the customer. After all, it's the customer who knows about the old data and business rules. If you absolutely need to migrate the existing data, don't underestimate the effort of having to map out the existing organically evolved "solution". That may be a project in it's own right.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-4329955436004163545?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/4329955436004163545/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=4329955436004163545' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/4329955436004163545'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/4329955436004163545'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2008/05/lessons-from-it-projects-migrating.html' title='Lessons from IT projects: migrating existing data to a new system'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-4975522519608308206</id><published>2008-02-24T18:48:00.001+01:00</published><updated>2010-01-12T02:00:44.669+01:00</updated><title type='text'>Logging all your chats in one place</title><content type='html'>I &lt;strike&gt;currently use&lt;/strike&gt; used to use &lt;a href="http://www.pidgin.im"&gt;pidgin&lt;/a&gt; for chatting, mainly because it is cross-platform and I liked using the same application on windows and linux. Another advantage of pidgin was that it is designed to support almost all the chat protocols out there. I can be online on MSN and google talk at the same time with the same application.&lt;br /&gt;&lt;br /&gt;&lt;img title="screenshot of pidgin add account dialog" src="http://lh4.google.com/wcoenen/R8Gyb2Drb1I/AAAAAAAABMI/4XDSKQXG9JM/s400/newpidginaccount.png" /&gt;&lt;br /&gt;&lt;br /&gt;One thing has been bugging me for a while now though, and that's the fact that my pidgin chat logs are stored locally. &lt;span style="font-weight:bold;"&gt;"Locally"&lt;/span&gt; turns out to mean &lt;span style="font-weight:bold;"&gt;"spread out over several machines and user accounts"&lt;/span&gt;, without any hope of making them searchable or properly backed up. This is not a good thing. Fortunately this is only a problem for MSN chats. It doesn't matter for my google talk account, as the google talk server will log everything for me "in the cloud" and even make it searchable from the gmail web interface.&lt;br /&gt;&lt;br /&gt;&lt;img title="screenshot of chat logs in gmail web interface" src="http://lh6.google.com/wcoenen/R8HE7WDrb2I/AAAAAAAABMQ/8UnLbqc0E8U/s800/googletalklog.png" /&gt;&lt;br /&gt;&lt;br /&gt;Is there anyway to get this advantage for other chat accounts also? Well actually, it turns out that there is. Google talk is just an jabber/XMPP implementation, and google has enabled "federation" with other jabber servers. That means that anyone can run their own jabber server, create users for it, and chat with google chat users. The jabber spec also includes the concept of "transports", which are basically gateway services running on the jabber service which enable you to talk to other networks like MSN. All you need to do is register an MSN transport with your google talk account, do all your MSN chatting via your google account, and voila: all your MSN chats are stored at google.&lt;br /&gt;&lt;br /&gt;Here's how to do it:&lt;br /&gt;1) Google talk or pidgin doesn't enable you to add transports. Download and install &lt;a href="http://psi-im.org"&gt;psi&lt;/a&gt; instead, another jabber client.&lt;br /&gt;2) Psi will prompt you to add a jabber account when you first start it. Use your gmail address as a jabber-id. Set the status to "online", you should be connected to google talk now.&lt;br /&gt;3) Right click on gmail.com in psi and click "service discovery". Psi will automatically look for services at "gmail.com" and show an error message because there aren't any.&lt;br /&gt;4) Enter another jabber server in the service discovery window, one which runs an MSN transport (e.g. jabber.belnet.be) or some other transport for your favorite chat network.&lt;br /&gt;5) Locate the MSN transport in the list of services, right-click it and click "register". This will prompt you for your MSN credentials.&lt;br /&gt;&lt;img src="http://lh6.google.com/wcoenen/R8HJCWDrb3I/AAAAAAAABMY/MlaYgzN4hR8/s400/registermsntransport.png" /&gt;&lt;br /&gt;6) Add/Authorize the transport contact which now appears in your main psi window (the "contact roster"). Double click it and allow it to subscribe to your presence.&lt;br /&gt;7) All your MSN contacts will now also appear, each one needs to be accepted for addition in your contact list. (It may be faster to close psi at this point, log in with pidgin and accept the contacts from there.)&lt;br /&gt;8) Say something to one of your MSN buddies&lt;br /&gt;9) Go to gmail.com, log in, click "chats" or search for what you just said. It's all there! :) All your MSN chats are now logged by google, as long as you use your name@gmail.com jabber account for chatting.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-4975522519608308206?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/4975522519608308206/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=4975522519608308206' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/4975522519608308206'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/4975522519608308206'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2008/02/logging-all-your-chats-in-one-place.html' title='Logging all your chats in one place'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-4326024349411274169</id><published>2008-02-21T23:03:00.000+01:00</published><updated>2008-02-22T00:37:11.323+01:00</updated><title type='text'>Finding debian packages and listing the files installed by them</title><content type='html'>&lt;a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://www.debian.org"&gt;&lt;img style="float:right; margin:0 0 10px 10px;cursor:pointer; cursor:hand;width: 90px;" src="http://www.debian.org/logos/banner_64.gif" border="0" alt="" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;The great thing about GNU/linux distributions is that they come with zillions of software packages, downloadable over the internet from free software repositories. Say you notice that your firefox browser doesn't support java applets. In &lt;a href="http://www.debian.org"&gt;debian&lt;/a&gt;, you're just a&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;apt-cache search java mozilla&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;away from finding a package providing just what you need. One of the results returned on my machine by the above command was "sun-java6-plugin". Just do&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;apt-get install sun-java6-plugin&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;and you're done! No need to worry about dependencies (like, ehm, java!), those are also downloaded and installed as needed.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Using an installed package&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;When you install an application "foo", typically it will be in a package named "foo" and will be launchable by typing "foo" in the console. In the case of command line tools, typing "man foo" will bring up one of those fascinating manual pages. In the case of GUI tools, they should be automagically added to your start menu in your favorite window manager. So far so good.&lt;br /&gt;&lt;br /&gt;Unfortunately, a minority of the packages are a bit evil in this aspect. The "bittorrent" package for example provides the original bittorrent implementation as a set of command line tools. You can install it, but you'll find that no "bittorrent" command becomes available, nor will "man bittorrent" give any results. "apt-cache show bittorrent" doesn't provide much hints either. Where the hell are my command line tools?!&lt;br /&gt;&lt;br /&gt;There is a solution! It took me a while to find, but it is to do a&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;dpkg -S bittorrent|grep bin&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;which will list all the invokable binaries installed by the bittorrent package. Turns out that the tool is invokable as btdownloadcurses.bittorrent (thank you tab completion!).&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Getting started with a new programming library&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;This is also very handy for other types of packages for which it may not be obvious how to start using them, like programming libraries. Suppose you want to write a little script to upload your pictures to &lt;a href="http://picasaweb.google.com"&gt;google's picasaweb service&lt;/a&gt;. A quick&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;apt-cache search picasa&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;will yield the "python-gdata" package. Once installed,&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;dpkg -S python-gdata&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;will help you locate all the new python goodness at your disposal. In this case, it turns out that alot of sample scripts are installed at /usr/share/doc/python-gdata to help you get started.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-4326024349411274169?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/4326024349411274169/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=4326024349411274169' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/4326024349411274169'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/4326024349411274169'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2008/02/finding-debian-packages-and-listing.html' title='Finding debian packages and listing the files installed by them'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-675744323744280651</id><published>2008-02-04T21:29:00.000+01:00</published><updated>2008-02-04T23:29:58.109+01:00</updated><title type='text'>Recording phone calls on a S60 phone. Part 3: getting rid of the beep</title><content type='html'>OK so recording phone calls turned out to be relatively easy, but what about that annoying beep inserted into recorded phone conversations? This beep comes after the first second of the conversation, and every 15 seconds after that. Turns out that there is a way around this: the trick to avoid the beep is to not record longer than 1 second.&lt;br /&gt;&lt;br /&gt;That's a pretty short conversation, you say?&lt;br /&gt;&lt;br /&gt;Not necessarily. You could, for example, stop the recording before the first second completes, then restart it immediately, etc... Unfortunately the speed at which code can be executed is limited, and this technique results in unacceptable gaps in the recording, especially if we use python. There is a commercial application called &lt;a href="http://www.killermobile.com/newsite/top-content/mobile-software/total-recall-%11-call-recording-software-for-s60-devices.htm"&gt;Total Recall&lt;/a&gt; which appears to use this technique. It worked for my N95 if I set the recording quality to "low", but like my python script, the recording gaps are unacceptable.&lt;br /&gt;&lt;br /&gt;Is it possible to do better? Yes! If you start a new recording before stopping the previous one, the beep still doesn't surface, and there will be no lost audio. The following function does exactly that:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;def start_recording(filename):&lt;br /&gt;   global recording&lt;br /&gt;   counter = 0&lt;br /&gt;   recording = True&lt;br /&gt;   sound_file = audio.Sound.open(filename+'_part'+str(counter)+'.wav')&lt;br /&gt;   sound_file.record()&lt;br /&gt;   start = True&lt;br /&gt;   while recording:&lt;br /&gt;      if start:&lt;br /&gt;         start = False&lt;br /&gt;      else:&lt;br /&gt;         e32.ao_sleep(.6)&lt;br /&gt;      counter = counter + 1&lt;br /&gt;      next_sound_file = audio.Sound.open(filename+'_part'+str(counter)+'.wav')&lt;br /&gt;      next_sound_file.record()&lt;br /&gt;      e32.ao_sleep(.3)&lt;br /&gt;      sound_file.stop()&lt;br /&gt;      sound_file.close()&lt;br /&gt;      sound_file = next_sound_file&lt;br /&gt;   print "stopping recording"&lt;br /&gt;   sound_file.stop()&lt;br /&gt;   sound_file.close()&lt;br /&gt;   concatenate_wavs(filename, counter)&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;This function will create a recording in fragments of less than 1 second, which overlap about .3 seconds. In practice, it turns out that the overlap is typically a bit less than that. Now we could post-process these audio fragments on a PC to stitch them together, but part of the usefullness of recording phone conversations is that you can immediately replay them on your phone. This implies the need to do the post-processing on the phone itself.&lt;br /&gt;&lt;br /&gt;This is were the &lt;a href="http://docs.python.org/lib/module-wave.html"&gt;python wave module&lt;/a&gt; comes in. This module provides all the functionality you need to read and write valid .wav files, but unfortunately it is not included in the PyS60 implementation. Not one to be easily discouraged, I &lt;a href="http://www.python.org/download/"&gt;downloaded the official python 2.2 sources&lt;/a&gt;. This is the (old!) version upon which PyS60 is based. Unpack the source archive, copy the wave.py and chunk.py modules to the phone, and yes, it works!&lt;br /&gt;&lt;br /&gt;Simply concatenating the wave samples will result in a little stutter in the recording though. The following function attempts to detect the overlap between fragments to fix this:&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;def concatenate_wavs(filename, max):&lt;br /&gt;   print "Processing "+str(max+1)+" audio chunks:"&lt;br /&gt;   merged = wave.open(filename+'.wav','w')&lt;br /&gt;   previousframes = None&lt;br /&gt;   for i in range(0,max+1):&lt;br /&gt;      partname = filename+'_part'+str(i)+'.wav'&lt;br /&gt;      part = wave.open(partname,'r')&lt;br /&gt;      frames = part.readframes(part.getnframes())&lt;br /&gt;      part.close()&lt;br /&gt;      os.remove(partname)&lt;br /&gt;      if i==0:&lt;br /&gt;         merged.setparams(part.getparams())&lt;br /&gt;         overlap=0&lt;br /&gt;      else:&lt;br /&gt;          overlap=frames.find(previousframes[-8:])+8&lt;br /&gt;          if overlap==7:&lt;br /&gt;              overlap=0&lt;br /&gt;          print overlap&lt;br /&gt;      previousframes=frames&lt;br /&gt;      merged.writeframes(frames[overlap:])&lt;br /&gt;      &lt;br /&gt;   merged.close()&lt;br /&gt;   print "done!"&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;&lt;br /&gt;And there you go, &lt;a href="http://docs.google.com/Doc?id=dgffw84v_21g7x4s4c2"&gt;a simple python script for recording phone conversations on a S60 3rd edition phone without beeps&lt;/a&gt; :) For some reason the overlap detection does not always work, but the casual stutter is not yet annoying enough to warrant further investigation. More important is the fact that this script still needs alot of work before it can be declared suitable for public consumption. It should get a proper GUI, be &lt;a href="http://www.nbl.fi/~nbl928/ensymble.html"&gt;packaged as a standalone application which can be easily installed&lt;/a&gt;, and it should be started automatically whenever the phone is booted.&lt;br /&gt;&lt;br /&gt;But I have not yet figured out all that stuff, so that's for another post... :)&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-675744323744280651?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/675744323744280651/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=675744323744280651' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/675744323744280651'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/675744323744280651'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2008/02/recording-phone-calls-on-s60-phone-part.html' title='Recording phone calls on a S60 phone. Part 3: getting rid of the beep'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-5113700073555437725</id><published>2008-01-25T00:48:00.001+01:00</published><updated>2008-02-04T23:39:54.424+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='symbian nokia'/><category scheme='http://www.blogger.com/atom/ns#' term='symbian'/><title type='text'>Recording phone calls on a S60 phone. Part 2: working prototype</title><content type='html'>&amp;lt;voice type="farnsworth"&amp;gt;Good news everyone!&amp;lt;/voice&amp;gt;&lt;br /&gt;&lt;br /&gt;I messed around with the PyS60 python interpreter for my S60 phone, and managed to record phone calls! The first step is to install PyS60 as described on the &lt;a href="http://wiki.opensource.nokia.com/projects/PyS60"&gt;PyS60 wiki&lt;/a&gt;. No further tools are needed, the python runtime and shell on your phone are sufficient. Next, we need a script which listens for phone calls being connected, either incoming or outgoing, and which starts and stops recording those calls:&lt;br /&gt;&lt;tt&gt;&lt;pre&gt;&lt;br /&gt;import appuifw&lt;br /&gt;import e32&lt;br /&gt;import telephone&lt;br /&gt;import audio&lt;br /&gt;import time&lt;br /&gt;&lt;br /&gt;# Must be either None, or a sound file which is recording.&lt;br /&gt;sound_file = None&lt;br /&gt;&lt;br /&gt;# Directory used to save call recordings. Must exist.&lt;br /&gt;# Filename will be appended to this path.&lt;br /&gt;recordings_folder = "e:\\CallRecordings\\"&lt;br /&gt;&lt;br /&gt;def cb_calling(args):&lt;br /&gt;&lt;br /&gt;   global sound_file&lt;br /&gt;   state = args[0]&lt;br /&gt;&lt;br /&gt;   # if transition to connected state and not recording, start recording&lt;br /&gt;   if (state == telephone.EStatusConnected) and (sound_file == None):&lt;br /&gt;      number = args[1]&lt;br /&gt;      if number == None:&lt;br /&gt;         number = "None"&lt;br /&gt;      filename = recordings_folder + time.strftime("%Y%m%d_%H%M_%S_") + number + '.wav'&lt;br /&gt;      #filename = recordings_folder + 'test.wav'&lt;br /&gt;      print filename&lt;br /&gt;      sound_file = audio.Sound.open(filename)&lt;br /&gt;      sound_file.record()&lt;br /&gt;&lt;br /&gt;   # if transition to non-connected state and recording, stop recording&lt;br /&gt;   elif (state != telephone.EStatusConnected) and (sound_file != None):&lt;br /&gt;      sound_file.stop()&lt;br /&gt;      sound_file.close()&lt;br /&gt;      sound_file = None&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;def quit():&lt;br /&gt;    app_lock.signal()&lt;br /&gt;&lt;br /&gt;appuifw.app.exit_key_handler= quit&lt;br /&gt;app_lock=e32.Ao_lock()&lt;br /&gt;&lt;br /&gt;telephone.call_state(cb_calling)&lt;br /&gt;telephone.incoming_call()&lt;br /&gt;&lt;br /&gt;app_lock.wait()&lt;br /&gt;&lt;/pre&gt;&lt;/tt&gt;&lt;br /&gt;The body of the script only sets the exit key handler, registers a callback function for detecting telephone call state changes, and starts waiting for a signal on a lock object. After that, the main thread will not do any more work. It was surprising to discover that real multi-threading is central to developing in Pys60. I was expecting to see a "message queue with single threaded message pump" kind of model, like most PC application frameworks.&lt;br /&gt;&lt;br /&gt;Copying this python script to a \Python directory enables you to run it on the phone with the python shell application. Once running, it will listen for phone calls and record them to the recordings_folder.&lt;br /&gt;&lt;br /&gt;Unfortunately, there is an annoyance: apparently nokia feels obligated to insert a beeping sound into the call every 5 seconds if it is being recorded. The beeping is not present in the recording though. I did not find any more information on this "feature", other than that it appears to be built into the phone. I guess nokia is trying to legally cover their ass here. I researched &lt;a href="http://www.juridat.be/cgi_loi/loi_a.pl?language=nl&amp;caller=list&amp;cn=1994063049&amp;la=n&amp;fromtab=wet&amp;sql=dt=%27wet%27&amp;tri=dd+as+rank&amp;rech=1&amp;numero=1"&gt; Belgian law regulating phone call recording&lt;/a&gt;, but did not find any obligation to inform participants that you are recording them. If you are not recording with intent to deceive or harm and are participating in the call yourself, then it's OK to record without warning. (Disclaimer: I am not a lawyer)&lt;br /&gt;&lt;br /&gt;Can we get rid of these annoying beeps? Is there a way to automatically start the call recorder code on phone boot? Questions, questions...&lt;br /&gt;&lt;br /&gt;To be continued!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-5113700073555437725?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/5113700073555437725/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=5113700073555437725' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5113700073555437725'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/5113700073555437725'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2008/01/recording-phone-calls-on-s60-phone-part.html' title='Recording phone calls on a S60 phone. Part 2: working prototype'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-3673058489179456797</id><published>2008-01-23T17:00:00.000+01:00</published><updated>2008-01-25T00:48:49.201+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='nokia'/><category scheme='http://www.blogger.com/atom/ns#' term='symbian'/><title type='text'>Recording phone calls on a S60 phone.</title><content type='html'>I love gmail. I haven't needed to delete email since I started using it. And the search feature is awesome. I wish my entire life could be archived and searched just as easily. And why not? For example, everything I hear could in principle be recorded to files and archived. Add a dash of speech-to-text and I could have a searchable history of all my conversations.&lt;br /&gt;&lt;br /&gt;Right now however, I only archive email and the occasional cameraphone snapshot or video (and chat logs and text messages, but I should archive them better). In pursuit of the goal "log everything", I'm going to try and add something to that list: phone conversations.&lt;br /&gt;&lt;br /&gt;My nokia smartphone is build on the S60v3 symbian platform. There exists a &lt;a href="http://pys60.sf.net"&gt;python interpreter&lt;/a&gt; for that platform, and it comes with modules for doing a lot of stuff. According to the documentation, you can run code in response to incoming calls. You can also record audio. This should be all I need to start logging my phone calls.&lt;br /&gt;&lt;br /&gt;Yay, we've got ourselves a project :)&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-3673058489179456797?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/3673058489179456797/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=3673058489179456797' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/3673058489179456797'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/3673058489179456797'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2008/01/recording-phone-calls-on-s60-phone.html' title='Recording phone calls on a S60 phone.'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-3588895764424777698</id><published>2007-07-16T16:55:00.000+02:00</published><updated>2008-01-24T10:50:31.703+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='dotNET'/><title type='text'>Using System.Diagnostics.StackTrace to leave debugging clues.</title><content type='html'>When I am debugging, I often find myself wishing I could step &lt;span style="font-style:italic;"&gt;back&lt;/span&gt; to a certain point, in order to discover at which point some piece of data becomes corrupted. Most of the time, it is possible to set a conditional breakpoint which will break the program at the point where something unexpected happens, and reproduce the bug again.&lt;br /&gt;&lt;br /&gt;However, this does not always work. Let's say for example that some piece of code is calling &lt;b&gt;Dispose&lt;/b&gt; on an instance which it doesn't own, and therefore shouldn't dispose. Things will go haywire when the instance is used after being disposed. At that point, it may be hard to determine the culprit who called &lt;b&gt;Dispose&lt;/b&gt;. And because of the number of instances being created and disposed, it may very well be impractical to set a breakpoint in &lt;b&gt;Dispose&lt;/b&gt; and inspect each call to it!&lt;br /&gt;&lt;br /&gt;What we need here is a way to see who called &lt;b&gt;Dispose&lt;/b&gt; in the past.&lt;br /&gt;The way I do this in .NET is by setting a field to a new &lt;a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemdiagnosticsstacktraceclasstopic.asp"&gt;System.Diagnostics.StackTrace&lt;/a&gt; in &lt;b&gt;Dispose&lt;/b&gt; just for debugging. Then when things go haywire and the debugger breaks, I simply read the stacktrace to discover the culprit. This trick can be used in any programming language or environment which supports the runtime generation and inspection of stacktraces.&lt;br /&gt;&lt;br /&gt;In visual studio 2005, you could also avoid modifying your code by inserting a tracepoint with the following expression: &lt;blockquote&gt;Instance with hashcode {GetHashCode()} was disposed. Call stack: $CALLSTACK&lt;/blockquote&gt; Then when things go haywire, you can inspect the hashcode of the corrupted instance and search for it in the trace log.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-3588895764424777698?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/3588895764424777698/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=3588895764424777698' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/3588895764424777698'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/3588895764424777698'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2007/07/using-systemdiagnosticsstacktrace-to.html' title='Using System.Diagnostics.StackTrace to leave debugging clues.'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-164254790613323783</id><published>2007-05-28T15:05:00.000+02:00</published><updated>2007-05-28T15:35:05.920+02:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='sports'/><category scheme='http://www.blogger.com/atom/ns#' term='running'/><title type='text'>20 kilometers of Brussels</title><content type='html'>Like last year, I participated in the &lt;a href="http://en.wikipedia.org/wiki/20_km_of_Brussels"&gt;20 kilometers of Brussels&lt;/a&gt; yesterday along with some of my coworkers. I had my sights set on finihsing within 1h40m.&lt;br /&gt;&lt;br /&gt;Up to the 17th kilometer I was perfectly on schedule for that time. Unfortunately I got recurring cramps in my calf muscles during the last 3 kilometers, which almost halved my speed :( I still finished in 1h46m15s. That's an improvement of 2 minutes over last year, so the result was not a complete disappointement.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-164254790613323783?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/164254790613323783/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=164254790613323783' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/164254790613323783'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/164254790613323783'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2007/05/20-kilometers-of-brussels.html' title='20 kilometers of Brussels'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-1143415379421433159.post-3679408685894714200</id><published>2007-05-16T17:35:00.000+02:00</published><updated>2008-01-24T10:51:17.727+01:00</updated><category scheme='http://www.blogger.com/atom/ns#' term='dotNET'/><title type='text'>Don't document that you throw ArgumentException or InvalidOperationException</title><content type='html'>When &lt;a href="http://en.wikipedia.org/wiki/Programming_by_contract"&gt;programming by contract&lt;/a&gt; (which should always be the case, at least implicitly), each method has pre-conditions that the caller should adhere to and post-conditions which the callee guarantees if the pre-conditions were satisfied; if they weren't, all bets are off.&lt;br /&gt;&lt;br /&gt;Let's look at an example in the .NET framework: &lt;a href="http://msdn2.microsoft.com/en-us/library/ms162529%28VS.80%29.aspx"&gt;System.XML.XmlReader.ReadElementContentAsString&lt;/a&gt;. This method has the precondition that the xml reader should be positioned on an element. However, Microsoft has chosen to actually document this as a postcondition: if the xml reader is not positioned on an xml element, InvalidOperationException is thrown. This is the normal documentation style for the .NET framework, but it has never sat right with me for two reasons.&lt;br /&gt;&lt;br /&gt;The first reason is that you shouldn't encourage programmers to rely on avoidable exceptions. I feel it would be better if this was simply documented as a precondition which must never be violated. What happens when the caller violates the contract doesn't need to be documented, so the guarantee of an InvalidOperationException could be omitted. It would of course still be thrown, but as an indication of a programming error. It shouldn't be relied upon.&lt;br /&gt;&lt;br /&gt;Another reason is inheritance. One of the great things of programming by contract is that it provides rules for what you can do in an overriding method. Any method must implement the contract of the method it overrides, because the caller may not be aware of the exact type of the callee. However, the overriding method may loosen the pre-conditions. It must merely accept anything that the overridden method would have accepted. It may accept more. Unfortunately, if the preconditions are documented as exceptions, they are now post-conditions which restrict the overriding method without good reason.&lt;br /&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;Conclusion: don't document ArgumentException and InvalidOperationException if it is sufficient to document pre-conditions.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href="http://msdn2.microsoft.com/en-us/library/ms162529%28VS.80%29.aspx"&gt;&lt;/a&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/1143415379421433159-3679408685894714200?l=mindinthewater.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://mindinthewater.blogspot.com/feeds/3679408685894714200/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=1143415379421433159&amp;postID=3679408685894714200' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/3679408685894714200'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/1143415379421433159/posts/default/3679408685894714200'/><link rel='alternate' type='text/html' href='http://mindinthewater.blogspot.com/2007/05/dont-document-that-you-throw.html' title='Don&apos;t document that you throw ArgumentException or InvalidOperationException'/><author><name>Wim Coenen</name><uri>http://www.blogger.com/profile/16314282132983289163</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='http://1.bp.blogspot.com/-0_JaF5Oe1ys/TkEvAXdUOmI/AAAAAAAAEt4/b4dXJjFQI-4/s220/snapshot-float2-smallsquare.png'/></author><thr:total>0</thr:total></entry></feed>
