Monday, January 29, 2007

Working with Setup projects

One thing I don't have much experience with is the Setup project type in Visual Studio. Most of my previous experience comes from writing web applications, so there was no real need to write an installer package.

But now that I've been tasked over the past 6 months or so with writing a desktop application, I've had to learn a lot of new tricks. The Windows Installer is one of those tricks. Unfortunately, the documentation for the actual Setup project type itself is rather abysmal and mostly just links into documentation for the Windows Installer SDK.

So today I was trying to figure out how to set default values for my custom properties. Unfortunately, the interface inside Visual Studio has no means for doing this. I did some reading, and I found that the tool I want is called Orca.

Orca is part of the Windows Platform SDK. It's no small download, but it contains lots of useful tools and documentation, of which the Windows Installer SDK is a part. Orca is a handy little editor that lets you modify the tables inside a .MSI package (among other things).

To set defaults for my properties, I just compile my .MSI package as normal within Visual Studio. Then I can open it with Orca, go to the "Property" table, and create the properties I need.

One thing to note as well: the case of properties makes a difference. A property that is in all uppercase (like PROPERTYNAME) is considered a "public" property; public properties can be adjusted at install-time using command line switches, like follows:



msiexec /i Example.msi PROPERTYNAME=VALUE



A property with at least one lower case letter (like PropertyName) is considered "private" and cannot be modified at install-time by the user.

There's also another type of property you might find useful, called "restricted public." These are just like public properties, except that their values can only be modified by a system administrator. To make a restricted public property, just define it as a public property. Then, create a new property called SecureCustomProperties (or edit it if it exists). Set the value of this property to the names of the public properties you want to restrict, separated by semicolons if there is more than one.

Anyways, thats' the basic run-down. In the future I'm going to look into ways to automate this process.

Wednesday, January 24, 2007

When are attributes created?

Speaking of Attributes, there's often a lot of misconceptions about how they are handled by the compiler and at runtime. Some of this is because of not reading the documentation properly, but also a lot of it is compounded by the fact that there are actually different kinds of attributes that look the same but are handled differently by the compiler and the CLR.

Take, for instance, ObsoleteAttribute: this attribute, innocently enough, behaves like any other custom attribute. It is written into the compiled metadata for your assembly. It can be reflected at runtime. But, as you know, it also triggers a specific response in the compiler; it generates a warning or error informing the programmer that the marked element is deprecated.

This can generate a false conception for the developer that attributes are instantiated a compile-time by the compiler. This is not true; ObsoleteAttribute just happens to be a special case handled specifically by the compiler.

You should also be aware of a type of attribute known as a "pseudo-attribute." These attributes are not written to the compiled metadata for your assembly, and thus cannot be reflected at runtime. Again, they are typically interpreted by the compiler in some way.

For example, there is the pseudo-attribute AssemblyVersionAttribute. This attribute tells the compiler what version to assign to the final assembly produced. If you attempt to reflect this attribute at runtime, you will find that it doesn't actually exist on your assembly (you have to use Assembly.GetName().Version).

The ultimate thing you have to keep in mind is that attributes are not instantiated until someone requests them. The compiler does this on its own in certain cases, but for the most part it just does a compile-time verification of the code and then passes the attribute on as meta-data into the IL.

For an example, assume that we have defined a custom attribute called TimeStampAttribute that takes a single string in its constructor that it wants to parse into a DateTime value. We want to use this to mark when we created certain methods:

C#:

[TimeStamp("fred")]
public void MyMethod()
{
// ...
}


VB.NET:

<TimeStamp("fred")> _
Public Sub MyMethod()
' ...
End Sub


But wait! That code won't work, right? It's supposed to have a date/time that we can parse like "1/24/2007 11:46 PM" instead of "fred." We're going to get a FormatException as soon as we run, right? Wrong.

The code of course compiles just fine, since "fred" is a string like we asked for. The code will even run just fine for the most part since the attributes aren't created explicitly. You won't get the expected exception raised until you make a call to GetCustomAttributes() that ends up iterating over the attributes in question.

Tuesday, January 23, 2007

When iterating attributes, make sure you get the right thing

I'll admit I kind of dubbed out on this one. When you're trying to get the custom attributes off of an object, make sure you're getting the right object. In my case, I needed to get the Attributes of my assembly, not the Assembly class itself.

It should come as no surprise that the following code doesn't do what I expected:



object[] attribs = Assembly.GetExecutingAssembly().GetType().GetCustomAttributes(typeof(Attribute), false);



Instead I just needed to remove the call to GetType():



object[] attribs = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(Attribute), false);



The Assembly class provides a handy GetCustomAttributes() method since calling it on the Assembly type itself doesn't really help you much. The first example gets you a bunch of crazy attributes that really aren't what I was looking for; the second returns all of the assembly-decorating attributes declared in my AssemblyInfo.cs.

Saturday, January 20, 2007

Attempting to reach remote resources with C#

This is related to the project in my previous post. Now that we had the IVR working correctly, we needed a way to move the voice recordings from the IVR server to a web server where our customers can retrieve them. Last year I wrote a web service called ResourceProxy that accomplishes the task of moving files from one server to another very nicely and with very strict security restrictions (it requires authentication, it can restrict the paths and file types allowed, etc.). Unfortunately, the IVR is not a web server, it's a phone server. So installing IIS to allow web connectivity was not a solution we wanted to use.

So what we have are two file shares. One is read-only and allows me to pick up the recordings from the phone server, and a second is read/write and allows me to drop them off on the web server. I have to authenticate using two different domain accounts to access each share, and of course the web service that accomplishes this is running as the ASP.NET machine account.

So this creates a small problem. How does one authenticate a process running as the ASP.NET machine account to allow it to connect to remote servers? Looks like we need to get down and dirty with P/Invoke.

My first solution was elegant. I would use LogonUser to authenticate each account, impersonate the principal, and then connect to the file share UNCs. The code looks something like this:


[DllImport("advapi32.dll")]
private static int LogonUser(string lpszUsername, string lpszDomain,
string lpszPassword, int dwLogonType, int dwLogonProvider,
ref IntPtr phToken);

[DllImport("advapi32.dll")]
private static int DuplicateToken(IntPtr hExistingToken, int ImpersonationLevel,
ref IntPtr phNewToken);

public void ImpersonateUser(string domain, string user, string password)
{
IntPtr token = IntPtr.Zero;

int result = LogonUser(user, domain, password,
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT, ref token);

if (result != 0)
{
IntPtr newToken = IntPtr.Zero;
result = DuplicateToken(token,
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
ref newToken);

if (result != 0)
{
WindowsIdentity id = new WindowsIdentity(newToken);
WindowsImpersonationContext context = id.Impersonate();
}
}
}


By the way, that's the incredibly pruned-down code. If you want a more extensive example, look at the WindowsImpersonationContext class.

This code worked beautifully. I could impersonate each of the domain accounts I needed to access the remote UNCs and copy my files over. Basically I just had to impersonate the first user, load the file into a MemoryStream, impersonate the other user, and write it to disk. (This was an acceptable approach because the files in question are always fairly small, typically less than 50k.)

It worked great on the development server, which is a Windows 2003 machine. Unfortunately, the production web server is still running Windows 2000. When we tried it there, everything broke. Argh!

For some reason, on Windows 2000 I was getting a bad username/password every time I tried to impersonate one of the domain principals, even though the exact same username/password combos worked fine on Windows 2003. I tried several different permutations of parameters to the LogonUser function to see if that would help. Eventually I ended up locating a Windows 2000 box on our development domain and tried my code there, just to see if it was an environment issue in production. It didn't work there either. Same code, same users and passwords; it just wouldn't work when moving from Windows 2003 to Windows 2000.

One thing that is mentioned on the page for LogonUser at MSDN is that on Windows 2000, the account executing LogonUser must have the SE_TCB_NAME privilege. You can grant this privilege to an account (such as the ASP.NET machine account in my case) by going in to the Local Security Policy for the server and adding the account to the "Act as a part of the operating system" policy. I tried this, and again it was no go.

So after fighting with that for a few hours, I decided to try a different approach. My boss mentioned that one technique he used in a classic ASP application to achieve the effect I wanted was to map the UNCs as network drives in the code before accessing them. This had some merit, so I investigated how to achieve this in C#.

The function we want this time is called WNetAddConnection2:


[DllImport("mpr.dll")]
private static int WNetAddConnection2(
ref NETRESOURCE lpNetResource,
string lpPassword, string lpUsername, int dwFlags);

public void MapNetworkDrive(string unc, string drive, string user, string password)
{
NETRESOURCE pRes = new NETRESOURCE();
int result = WNetAddConnection2(ref pRes, password, user, 0);
}


On the surface, this code looks a lot simpler, but I actually wrote a lot of peripheral code to make sure I wasn't trying to connect to the same resource twice and also to make sure I didn't map over an existing logical drive on the server. It doesn't hurt anything to map the resource twice, because WNetAddConnection2 will tell you what happened, but WNetAddConnection2 itself can be a somewhat costly operation, so I figured it better to avoid calling it if I can.

This code worked beautifully as well. After I had mapped the network shares I wanted, I could use the typical .NET IO classes to access the UNC directly without a problem.

Of course, after running it on Windows 2000, it broke again. Argh and double argh!

For some reason, on Windows 2000 this code kept throwing back an ERROR_WRONG_TARGET_NAME. According to the documentation I could find, this meant that the UNCs were not correct (target name referring the server destination). So why would the UNCs be correct on Windows 2003 but not on Windows 2000?

Who knows? At this point I was so frustrated with it I was willing to try anything. What I found is that if I specified the IP address (\\192.168.1.1\share as opposed to \\servername\share), Windows 2000 was perfectly happy with it. And now the code works fine in production, so I'm not going to play with it anymore.

Wednesday, January 17, 2007

How not to write an IVR

So for my first post here, let me take a minute to tell you a little about my most recent project at work. We're putting together an IVR for one part of a very large suite of applications. This suite of applications was supposed to be released on January 1st, but of course now it's January 17th and we're only just now getting to true integration testing.

Unfortunately we outsourced the IVR software to supposed "experts," which is what landed us in trouble. Their technical person who has been writing the software that controls the IVR script is a complete and utter tool. He doesn't know the basics of C++ let alone software development in general.

For instance, last night I sat on the phone with him for an hour-and-a-half whilst I debugged his code for him. I should point out that I am by trade a web designer and a managed code developer that normally uses Microsoft .NET. I have not touched C++ in about ten years, so I was using Google a lot and still filling in holes in his code. Meanwhile, the only insight he really offered was, "Well I can't imagine why it would be doing that!" Well, duh, that's why you're crappy code doesn't work, Einstein.

Basically the IVR script iterates through a bunch of prompts, gathering data from the caller, and posts each response to a web service. Should be pretty straightforward, right? He was convinced that it was my web service and not his code that could be the problem. Eventually I tracked it down to one prompt in the IVR that was destabilizing the entire application and causing it to crash (eventually crash, that is, which is why it was so damn hard to find). Code similar to the following was the culprit:

char filename[200];
memcpy(filename, basepath, 200);


What's wrong with this? We're copying one string into another, but the destination is not initialized or allocated to anything. He's literally writing bytes into no-man's land. Pointers and memory allocation are a hard concept to grasp for most beginning programmers, granted, but someone who works for a company writing C++ code full-time and charging ungodly amounts of money for that time should not be making such elementary errors.

Once I fixed this error for him, the program stopped crashing. Luckily I had hung up on him by that point, so he avoided my wrath.

While I'm at it, here's one more gem of his code (excuse me if it's not perfectly compilable, I'm doing this from memory):

j = 0;
for (i = 0; i < strlen(promptfiles); i++)
{
if (promptfiles[i] == '|')
{
memcpy(prompts[prompt_index], promptfiles + j, i - j);
j = i + 1;
prompt_index++;
}
}


This little nugget was meant to parse a string of IVR prompt sound files I was giving to him from the web service in a pipe-delimited format like:


Prompt1Name|ivr_prompts\prompt1.wav|Prompt2Name|ivr_prompts\prompt2.wav|...


According to him there was a problem with one of our prompts called "Timeout" that wouldn't play correctly. Can you spot the problem in his code? I'll give you a hint; "Timeout" appeared at the end of the pipe-delimited string.

The answer is pure lack of brainpower on his part. He only copies the part of the string he wants after he hits a pipe delimiter. The string wasn't terminated by a pipe, so he was just dropping off the last part of the string and not saving it.

I guess the real mystery is why he didn't just use strtok.

Anyways, that's my rant for this evening. I promise I'll start putting some happier and more informative posts here later; for now I just needed to get this off my chest.