Sunday, June 10, 2007

Powershell: The little interpreter language that could

Its been a long time since I learned a new technology that constantly interests me to tinker with in my free time.

Powershell, the .NET-based scripting language gives a fresh "scripting" approach to .NET.
Like Bash in unix, it relies heavily upon piping commands in a chain, but the pipe content is not stream-based it is object based, so all of the Powershell commands are polymorphic.

This is an example of a primitive "diff tool" I wrote in 20 clear lines:

$a = Get-Content c:\test.csv;
$b = Get-Content c:\test.xml;
$long = $a;
$short = $b;
if($b.Length -gt $a.Length) {
$long = $b;
$short = $a;
};

for($line = 1; $line -le $long.Length; $line++){
if($line -le $short.Length -and $short[$line-1] -ne $long[$line-1]){
"line [$line]:"
"one says: [" + $short[$line-1]
"the other says: [" + $long[$line-1]
}
elseif($line -gt $short.Length){
"line [$line]:"
"one says: [" + $long[$line-1]
}
}


Another example: these 6 lines of code display every Type in the CLR (9276 types)

[System.Object].Assembly.GetTypes() | foreach {
$typeCount++
$methodCount += $_.GetMethods().Length
"+--"+$_.Name
}
"Types $typeCount, Methods $methodCount"

A good crash course (I learned this way) can be found here:
http://blog.slaven.net.au/archives/2007/01/25/powershell-cheat-sheet/
If you havent seen powershell analyzer yet, then go check it out, becore it isnt free anymore!
http://www.powershellanalyzer.com/

Thursday, June 07, 2007

A Bad Day At Work Courtesy of: InstallShield

OK before I begin this tirade, I would like to acknowledge the following disclaimers:
  • I'm a bit of an odd person, I cant help it if my conclusions seem strange to normal readers
  • Opening someone's old code is great way to learn about the original programmer's cognitive process; every line was written for a reason (whether the reason is valid or not)
  • The MarcoVision company takes on a very ambitious challenge by wrapping the complexities of universal installation into a cutesy little drag and drop interface.
Today, I was given a broken installation project. The project was built in InstallShield by someone who clearly didnt know how to use it. I can't blame the guy, because I wish I knew less about InstallShield too.
Installshield has a native scripting language called "InstallScript". Its like VBScript and C had a tryst, and they had a bastard mutant baby. My condolences.
Here is a snippet for your viewing pleasure:

#include "UninstallScript.rul"
#include "_SdComponentTreeDlg.rul"
#define DEFAULT_DIR "C:\\Omnizone"
#define SUBDIRS "Share"
#define _UTIL_RUL_
if (1 = 1) then
//-cmp052705
SQLRTInitialize2 ();
nRetVal = SQLRTServerValidate( ISMSI_HANDLE );

nSize = MAX_PATH;
MsiGetProperty( ISMSI_HANDLE, "IS_SQLSERVER_STATUS", sTemp, nSize );
ShowMSI();
if( sTemp != "0" ) then
nSize = _MAX_PATH;
MsiGetProperty( ISMSI_HANDLE, "IS_SQLSERVER_STATUS_ERROR", sMessage, nSize );

if( nSize = 0 ) then
sMessage = SdLoadString( IDS_IFX_SQL_ERROR_LOGIN_FAILED );
endif;


You get the idea... Anyways, this code doesnt connect to a database. How do you connect to a database in this language? The database connection strings are stored within an in-memory database hosted by the MSI file during setup. Using MsiGetProperty and MsiSetProperty with the correct keywords, bit sizes, and variable type references (and a little bit of prayer) it SHOULD connect to the database but it doesn't.

Defect #1:
But don't worry! InstallShield gives you a "runtime script debugger" so you can quickly find problems in this rats nest!
So, how do I view the in-memory database for MSI values at runtime? Not supported!
Solution:
I wrote this code to give me insight:

prototype ShowMSI();
...
function ShowMSI()
string auth;
//string connection;
string server;
string db;
string userName;
string password;
string hidden;
string result;
number nSize;
begin
nSize = MAX_PATH;
MsiGetProperty( ISMSI_HANDLE, "IS_SQLSERVER_SERVER", server, nSize );
nSize = MAX_PATH;
MsiGetProperty( ISMSI_HANDLE, "IS_SQLSERVER_USERNAME", userName, nSize );
nSize = MAX_PATH;
MsiGetProperty( ISMSI_HANDLE, "IS_SQLSERVER_PASSWORD", password, nSize );
nSize = MAX_PATH;
MsiGetProperty( ISMSI_HANDLE, "MsiHiddenProperties", hidden, nSize );
nSize = 10;
MsiGetProperty( ISMSI_HANDLE, "IS_SQLSERVER_AUTHENTICATION", auth, nSize );
nSize = MAX_PATH;
MsiGetProperty( ISMSI_HANDLE, "IS_SQLSERVER_DATABASE", db, nSize );
result = "auth ["+auth+"] server ["+server+"] db ["+db+"] username ["+userName+"] password ["+password+"] hidden["+hidden+"]";

MessageBox (result, WARNING);
end;

Basically, any time I need to view a "snapshot" of the MSI database, I would call this function in the script, and a popup window would give me the variables I wanted to watch. Ugly, but at least it made debugging possible.

Defect #2:
After debugging with popup windows reminiscent of the Javascipt days before FireFox Javascript Debugger (Venkman), I found the problem was not with the extensive script that was written. The problem lied within one of InstallShield's native methods:

MsiGetProperty( ISMSI_HANDLE, "IS_SQLSERVER_STATUS", sTemp, nSize );
ShowMSI();
if( sTemp != "0" ) then
nSize = _MAX_PATH;
MsiGetProperty( ISMSI_HANDLE, "IS_SQLSERVER_STATUS_ERROR", sMessage, nSize );

if( nSize = 0 ) then
sMessage = SdLoadString( IDS_IFX_SQL_ERROR_LOGIN_FAILED );
endif;


MessageBox( sMessage, MB_OK );


The ISMSI_HANDLE is the MSI in-memory database, which stores things like the database name, user login, password, and authentication type.
SQLRTServerValidate attempte to connect to the database using the values in the MSI object. Problem is, it fails to connect to SQL Server 2005 databases where the hostname is "(local)" and the authentication is password-driven. Basically, this method is broken for a very common usage scenario. Piss me off.

The Solution:
Write a custom class that tests database connectivity in .NET, and using bubblegum and duct tape, connect the script to the .NET code, so it does what InstallShield was supposed to do (did I mention this is a very common usage scenario?)
The .NET code ended up looking like this:

using System;
using System.Data.SqlClient;
using System.Runtime.InteropServices;

namespace InstallationSupport
{


[ClassInterface(ClassInterfaceType.AutoDispatch)]
[Guid("4E0697CB-209A-40c5-939D-709924CC9AFB")]
public class DBConnectionValidator : IDBConnectionValidator
{
static string errMsg = "";

public DBConnectionValidator(){}


public int Check(string server, string username, string password, int windowsAuth)
{
int result = 0; // no error

SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();

try
{
server = server.Remove(server.IndexOf('\0'));
server = server.Replace("(local)", "localhost");
username = username.Remove(username.IndexOf('\0'));
password = password.Remove(password.IndexOf('\0'));
builder.DataSource = server;
builder.IntegratedSecurity = windowsAuth == 1;
if (!builder.IntegratedSecurity)
{
builder.UserID = username;
builder.Password = password;
}

SqlConnection conn = new SqlConnection(builder.ConnectionString);
conn.Open();
conn.Close();
}
catch (SqlException se)
{
errMsg = se.Message;
result = 1;
}
catch (Exception e)
{
errMsg = e.Message;
result = 1;
}

return result;
}

public string GetErr()
{
return errMsg;
}
}
[Guid("B464E841-D49A-4f5b-8701-0C40544CF110")]
public interface IDBConnectionValidator
{
int Check(string server, string username, string password, int windowsAuth);
string GetErr();
}
}

A few things to note:
  • This is a C# class library project
  • In the AssemblyInfo, I need to set "ComVisible" to true
  • In the project properties, I need to enable "Register for Com Interop" on the build settings
  • For some reason, I needed to make an interface so this class is more palatable to the install script (I dont know why, since the installscript doesnt reference the interface)
  • I added this class into the huge blob mass which is the Installation project (I place it in Setup Files/Billboards->Language Independent area)
  • I reference the dll in the code like this:
#include "ifx.h"
#include "util.h"
#include "isrt.h"
#include "iswi.h"
...
prototype number DBConnectionValidator.Check(BYREF STRING, BYREF STRING, BYREF STRING, NUMBER);
prototype string DBConnectionValidator.GetErr();
...
set oObj = CoCreateObjectDotNet(SUPPORTDIR ^ "InstallationSupport.dll", "InstallationSupport.DBConnectionValidator");

nRetVal = oObj.Check(sServer, sUser, sPassword, bWinAuth);
if( nRetVal != 0 ) then

szErrMsg = oObj.GetErr();
MessageBox( szErrMsg, MB_OK );


  • If you do anything even slightly wrong with building the .NET assembly or referencing it, it will yield the following exception: x80040707 DLL Function CrashedL ISRT._ CoCreateObjectDotNet That was helpful now wasnt it?
  • If you get anything wrong in your assembly, if you get anything wrong in the script, hell if you look at the computer with the wrong face, you will need to run a 5-10 minute complete rebuild of the setup project complete with paskaging as your punishment.
That was todays endeavor. For some reason, I really enjoy puzzles, but this one was not fulfilling in any stretch of the imagination, so I hope google will index this for some future person's benefit.
Oh, and to anyone from Macrovision: dont bother writing to tell me this feature is fixed with a patch or a new version, because
  • your upgrades cost too much and are too frequent
  • if this code didnt work, it should have never been released in the first place and you know Im right

Tuesday, June 05, 2007

Here is an interesting thought....

What if...

...C# had lambda expressions like lisp, scheme or sml?


...The DBMS had a native library for defining and managing the POCOs in your enterprise layer, eliminating SqlDataReaders and ODBC?


...I actually had the time to answer these questions?