Tuesday, October 21, 2008

DevTrack woes with build 1833 of mfc80.dll

We use TechExcel’s DevTrack tool to track our defects and project modifications.  A few weeks ago, one of our QA specialists installed SQL Server 2008 Express.  That broke DevTrack.  After SQL Express 2008 was installed, DevTrack would crash after loading.  After contacting DevTrack support, we were advised that the mfc80.dll installed with SQL Express was causing the problem. 

Their suggested work around?  They asked us to overwrite the version of mfc80.dll (8.0.50727.1833) located in one of the SQL Express folders with an older one (8.0.50727.762).  Ok, there is something wrong this picture.  You don’t replace a dll in another application’s folder.  That violates the integrity of the installation of the other application.  You have no idea of what the possible consequences will be for that application.  It could work fine, it could blow up in your face, or something in between.

The problem is that when DevTrack requests mfc80.dll, it’s being redirected through the WinSxS policy files to the latest and greatest version, version 8.0.50727.1833.  And there is something in MFC 1833 that just kills DevTrack.  The problem isn’t specific to DevTrack, apparently Corel’s WinDVD has the same problem.  I read a thread about people hacking the WinSxS policy files to force calls to mfc80.dll to use an older version.  That might work under XP, but will really break things under Vista.

I thought the Side-by-Side assembly model was supposed to eliminate “DLL Hell”.  For DevTrack to work, they have two choices.  They can patch their code so that it doesn’t crash with the 1833 version of mfc80.dll, or they can install their own local copy and not use the WinSxS version.  We’ve been waiting for a few weeks for a solution, that’s been a real frustrating point.

Monday, October 20, 2008

Installation is not configuration

Christopher Painter has a good post about the problems inherent with with having installers run SQL scripts.  Having an installer communicate with a database server just opens the door to all sorts of issues.  Just handling the connection to the server requires making sure that you have all of the required bits installed and that you can locate, and connect to the server.  None of that code is rocket science, we’ve been using it for years. I just don’t think having it in an installer is the right place for that type of code.

The problem is that you are running that code from inside a relatively fragile box, your installer.  Most installer authoring tools provide rudimentary support for running SQL scripts at install time.  They work just fine when all of your ducks are lined up in a row.   However your clients may be missing an odd duck or two, and you end up with fragile code wrapped inside a DLL that your installer will call.

I’ve always treated the installer package as just part of the actual installation process for the end user.  The main task of the installer is to get the bits of your application in place and handle any prerequisite runtime library your code may need.  When it comes to initializing or updating a database, I leave that to the main application and/or it’s support utilities.  if a SQL script is flawed and or doesn’t handle some edge condition that only one customer has, you can break the install.

By using a full blown application to handle the database task, your life (and your customer’s life) becomes much easier.  By running your own application, you have (or should have) a much richer environment for developing code.  You have complete control over the UI and you do not have to be concerned with trying match the UI style of the installer.   Also the testing and debugging of the database utility code becomes much easier as no longer need to account for the installer.

You also have the ability to run the application at any time after the install, without having to invoke the installer.  If it turns out the problem was a SQL script that didn’t work for that customer, you can immediately email or post online an updated SQL script, without forcing the user to run the installation process again.

We use a mixture of script based installers and Windows Installers and none of them make any attempt to run a SQL script.  I wrote a database utility application that gets launched after an install.  I wrote it when our company supported the MSDE, back in the day of SQL Server 7.  Back then, Microsoft provided absolutely zero for tools to manage the MSDE.  So this utility, by necessity needed to be able to attach and detach databases, back up and restore databases, manage the server and database logins, and perform schema updates. 

Over the years, this utility has matured and is very easy for the end user to use.  We provide all of schema updates in a single, compressed file.  For the use to apply an update the database schema, they just run my utility and it reads the update file and it knows which updates have already been applied and only runs the ones it needs to.

All very easy for the end use, but it would have been a nightmare to get that level of functionality running as part of the installer.  As soon as you add an outside dependency (in this case, the database server), you have added a point of failure that you will have absolutely no control over.

Friday, October 17, 2008

Strange 64-bit error with LayoutKind.Explicit

I have a C# service that collects data from another company that we do business with.  They send the data in a binary format from one of their C++ applications.  To read their data with .NET, I needed to marshal their data to a set of structs defined in C#.  I created a structure that looked something like this. 

    [StructLayout(LayoutKind.Explicit, Size = 48)]
public struct SampleHeader
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
[FieldOffset(0)]
public byte[] RecordType;

[MarshalAs(UnmanagedType.U4)]
[FieldOffset(8)]
public uint Version;

[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
[FieldOffset(12)]
public byte[] SystemCode;

[MarshalAs(UnmanagedType.U4)]
[FieldOffset(20)]
public uint LocalID;

[MarshalAs(UnmanagedType.U4)]
[FieldOffset(24)]
public uint HostID;
}



The actual struct had more fields, but this is enough to show the the problem. During development and testing, the code worked fine. Until we tried it on a 64-bit edition of Windows Server 2003. That's when it broke. As soon as I instantiated an instance of this struct, the service would throw an error. Something like this:



System.TypeLoadException: 
Could not load type 'SampleNameSpace.SampleHeader'
from assembly ''Sample, Version=1.2.3.4, Culture=neutral, PublicKeyToken=null'
because it contains an object field at offset 12 that is incorrectly aligned or overlapped by a non-object field.


To get it to fail, all I needed to do was to create a SampleHeader like this:



SampleHeader sh = new SampleHeader();



That didn't make any sense.  I couldn’t see any reason why it would work in 32-bit land, but not in 64-bit.  Since it was complaining about the “SystemCode” field, I commented out the other fields and played with the field offsets.  If I changed the offset from 12 to 16, I could create a SampleHeader object without any runtime errors.  Mind you, I could actually use it in my code, those offsets had to match the data my service was receiving.



So I went to plan “B”, getting rid of the explicitly laid out struct.  I created a new one without the StructLayout, MarshalAs, and FieldOffset attributes.  It looked like this:



    public struct SampleHeader
{
public byte[] RecordType;
public uint Version;
public byte[] SystemCode;
public uint LocalID;
public uint HostID;
}



Pretty much the same thing, except .NET defined the field alignments.  Instead of using marshalling to copy the data, I just used the BitConverter class.  I had already put the received data into a byte[] array, that made it easy to use BitConverter.  For this struct, I only needed the LocalID and HostID fields, so the following code was all that I needed:



    MyHeader.LocalID = BitConverter.ToUInt32(RawData, 20);
MyHeader.HostID = BitConverter.ToUInt32(RawData, 24);



This replaced the marshalling code that looked like this:



    GCHandle handle = GCHandle.Alloc(RawData, GCHandleType.Pinned);
SampleHeader MyHeader = (NewStuff)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(SampleHeader));
handle.Free();



I still don’t understand why Windows 64-bit  requires fields in a struct to be aligned on 4 byte boundaries, but the replacement code works and is easier to follow.

Tuesday, October 14, 2008

Restoring missing Build Events in Delphi 2007

If you have a Delphi 2007 project that was ported from Delphi 2006, then you may be missing the build event project options.  The .dproj file that Delphi 2006 creates is does not have a final XML element named PropertyGroup that Delphi 2007 uses.  Without that final PropertyGroup, Delphi 2007 will not enable Build Events as an option.  If you manually edit your .dproj file, just the following lines:

<PropertyGroup>
</PropertyGroup>


So that the .dproj files looks like this at the end of the file:



<PropertyGroup>
</PropertyGroup>
</Project>


After making that change, the next time you open that project with Delphi 2007 and select Options from the Project menu, you’ll see Build Events listed.  This only appears to happen if you migrate a Delphi 2006 project over to Delphi 2007.  If you create the project from scratch, you’ll see Build Events list.  That’s how I was able to determine what was missing, I just created a new project and compared the .dproj files.