Introducing SolarBeam

May 2nd, 2009

I don't usually code gui. It takes too much work to do it right and I can generally get a perfectly usable interface on the cli with much less effort. But exceptions can be made. These past few months I've been busy with a c# project written in Windows Forms. It's not a big application, but it's bigger than the kind of stuff I do most of the time, about 10kloc. I did quite relish this opportunity to try out Mono, which I've been meaning to do for some time.

solarbeam_diagrampane

The subject matter is solar diagrams. I guess there is no precise meaning for this phrase but I use it to talk about charts of the Sun's trajectory over the Earth. You have to be able to compute where the Sun is at any given time, and then you can draw these charts. Here's one for Utrecht.

solarbeam_diagram

The axis along the radius is the elevation. The other axis, along the circumference, is called the azimuth. With those two facts you can express where the Sun is in the sky. And the diagram depicts that trajectory with you, the observer, standing in the center. The application also tells you a few other related details, like the time of sunrise and sunset, dawn and dusk.

Technically speaking, it's a portable app, so you can plop it down anywhere on the file system and it runs. This was one of the key requirements actually, so that people can run it on their work machines or in labs, which tend to be restricted environments.

Diagrams can also be saved to image and scaled. Considering how much computation is going on there, I'm quite pleased with how responsive it turned out. There's also a funky clickable map and a collection of 2000 predefined locations, complete with timezone information.

solarbeam_map

Visit the project:

deploying a portable Mono app

April 22nd, 2009

The obvious advantage of coding in a byte compiled framework like .NET is that you don't have to mess with native dependencies (at least if you play your cards right). That makes your application maximally portable, just download and run anywhere the runtime runs.

So far so good, but if you get to the point where you want to offer more than just a zip file then unfortunately you have to deal with the tedious specifics of how applications are supposed to be installed on any particular platform. Granted, a portable app runs from anywhere, but what about launchers and shortcuts and whatnot?

solarbeam_createshortcuts

Well, one way of doing it is to tell the user "chuck the app anywhere" and then set up the links from within the app, wherever it may be on the filesystem. While a little unconventional, it obviates the need for a special installer app (which you don't need anyway cause you're not really installing).

What about moving the app? No problem, just overwrite the shortcuts.

On Windows

Let's start with the basics. How do you create a shortcut on Windows? It should be a trifle, right? Wrong, my young friend. This is .NET, we love making stuff surprisingly difficult. It actually takes tons of ridiculous voodoo, all of which is very unportable (I wonder if you could even get this to compile on Mono). The file format is binary (why make it simple, right?) and noone seems to know exactly how it works (although some guy tried to reverse engineer it), so you can't just write the 5 lines of code it should take and be done.

Fortunately, there is a simpler way. Instead of making yourself a .lnk file, you can get away with a .url file instead. It's a text format and it's simple. And it seems to work just as well.

So, create a .url, stick it on the user's Desktop and in the user's Start Menu (thankfully .NET does give you a way to lookup those paths).

On Linux

On Linux it's not immediately obvious where to look. As is often the case, the information "is out there", but where? freedesktop.org. But what have we here? A public specification? How delightfully convenient.

It turns out, though, that you need more than just a .desktop file. The .desktop file is sort of the "reference" to your app, but in order to stick it in the menu you need a .menu file too.

So, .desktop file goes into ~/.local/share/applications, .menu file goes into ~/.config/menus/applications-merged.

EDIT: It seems you don't even need the .menu file.

Have I missed anything?

emerge mono svn

March 31st, 2009

Yes, it's time for part two. If you're here it's probably because someone said "fixed in svn", and for most users of course that doesn't matter, but if you're a developer you might need to keep up with the latest.

Now, it's one thing to do a single install and it's another to do it repeatedly. So I decided to do something about it. Here is the script, just as quick and dirty and unapologetic as the language it's written in. To make up for that I've called it emerge.pl to give it a positive association.

What it does is basically encapsulate the findings from last time and just put it all into practice. Including setting up the parallel build environment for you. Just remember that once it's done building, source the env.sh file it spits out to run the installed binaries.

$ ./emerge.pl merge world

$ . env.sh

$ monodevelop &

This is pretty simple stuff, though. Just run through all the steps, no logging. If it fails at some point during the process it stops so that you can see the error. Then if you hit Enter it continues.

#!/usr/bin/perl
# Copyright (c) 2009 Martin Matusiak <numerodix@gmail.com>
# Licensed under the GNU Public License, version 3.
#
# Build/update mono from svn

use warnings;

use Cwd;
use File::Path;
use Term::ReadKey;


my $SRCDIR = "/ex/mono-sources";
my $DESTDIR = "/ex/mono";


sub term_title {
	my ($s) = @_;
	system("echo", "-en", "\033]2;$s\007");
}

sub invoke {
	my (@args) = @_;

	print "> "; foreach my $a (@args) { print "$a "; }; print "\n";

	$exit = system(@args);
	return $exit;
}

sub dopause {
	ReadMode 'cbreak';
	ReadKey(0);
	ReadMode 'normal';
}


sub env_var {
	my ($var) = @_;
	my ($val) = $ENV{$var};
	return defined($val) ? $val : "";
}

sub env_get {
	my ($env) = {
		DYLD_LIBRARY_PATH => "$DESTDIR/lib:" . env_var("DYLD_LIBRARY_PATH"),
		LD_LIBRARY_PATH => "$DESTDIR/lib:" . env_var("LD_LIBRARY_PATH"),
		C_INCLUDE_PATH => "$DESTDIR/include:" . env_var("C_INCLUDE_PATH"),
		ACLOCAL_PATH => "$DESTDIR/share/aclocal",
		PKG_CONFIG_PATH => "$DESTDIR/lib/pkgconfig",
		XDG_DATA_HOME => "$DESTDIR/share:" . env_var("XDG_DATA_HOME"),
		XDG_DATA_DIRS => "$DESTDIR/share:" . env_var("XDG_DATA_DIRS"),
		PATH => "$DESTDIR/bin:$DESTDIR:" . env_var("PATH"),
		PS1 => "[mono] \\w \\\$? @ ",
	};
	return $env;
}

sub env_set {
	my ($env) = env_get();
	foreach my $key (keys %$env) {
		if ((!exists($ENV{$key})) || ($ENV{$key} ne $env->{$key})) {
			$ENV{$key} = $env->{$key};
		}
	}
}

sub env_write {
	my ($env) = env_get();
	open (WRITE, ">", "env.sh");
	foreach my $key (keys %$env) {
		my ($line) = sprintf("export %s=\"%s\"\n", $key, $env->{$key});
		print(WRITE $line);
	}
	close(WRITE);
}


sub pkg_get {
	my ($name, $svnurl) = @_;
	my $pkg = {
		name => $name,
		dir => $name, # fetch to
		workdir => $name, # build from
		svnurl => $svnurl,
		configurer => "autogen.sh",
		maker => "make",
		installer => "make install",
	};
	return $pkg;
}

sub pkg_print {
	my ($pkg) = @_;
	foreach my $key (keys %$pkg) {
		printf("%14s : %s\n", $key, $pkg->{$key});		
	}
	print("\n");
}

sub pkg_action {
	my ($action, $dir, $pkg, $code) = @_;

	# Report on action that is to commence
	term_title(sprintf("Running %s %s", $action, $pkg->{name}));

	# Create destination path if it does not exist
	my ($path) = File::Spec->catdir($SRCDIR, $dir);
	unless (-d $dir) {
		mkpath($path);
	}

	# Chdir to source path
	my ($cwd) = getcwd();
	chdir($path);

	# Set environment
	env_set();

	# Perform action
	my ($exit) = &$code;

	# Chdir back to original path
	chdir($cwd);

	# Check exit code
	if ($exit == 0) {
		term_title(sprintf("Done %s %s", $action, $pkg->{name}));
	} else {
		term_title(sprintf("Failed %s %s", $action, $pkg->{name}));
		dopause();
	}
}

sub pkg_fetch {
	my ($pkg, $rev) = @_;
	
	if (exists($pkg->{svnurl})) {
		my $code = sub {
			return invoke("svn", "checkout", "-r", $rev, $pkg->{svnurl}, ".");
		};
		pkg_action("fetch", $pkg->{dir}, $pkg, $code);
	}
}

sub pkg_configure {
	my ($pkg) = @_;

	if (exists($pkg->{configurer})) {
		my $code = sub {
			my ($configurer) = $pkg->{configurer};
			if (!-e $configurer) {
				if (-e "configure") {
					$configurer = "configure";
				}
			}
			return invoke("./$configurer --prefix=$DESTDIR");
		};
		pkg_action("configure", $pkg->{workdir}, $pkg, $code);
	}
}

sub pkg_premake {
	my ($pkg) = @_;

	if (exists($pkg->{premaker})) {
		my $code = sub {
			return invoke($pkg->{premaker});
		};
		pkg_action("premake", $pkg->{workdir}, $pkg, $code);
	}
}

sub pkg_make {
	my ($pkg) = @_;

	if (exists($pkg->{maker})) {
		my $code = sub {
			return invoke($pkg->{maker});
		};
		pkg_action("make", $pkg->{workdir}, $pkg, $code);
	}
}

sub pkg_install {
	my ($pkg) = @_;

	if (exists($pkg->{installer})) {
		my $code = sub {
			return invoke($pkg->{installer});
		};
		pkg_action("install", $pkg->{workdir}, $pkg, $code);
	}
}


sub pkglist_get {
	my $mono_svn = "svn://anonsvn.mono-project.com/source/trunk";
	my (@pkglist) = (
		{"libgdiplus" => "$mono_svn/libgdiplus"}, 
		{"mcs" => "$mono_svn/mcs"}, 
		{"olive" => "$mono_svn/olive"}, 
		{"mono" => "$mono_svn/mono"}, 
		{"debugger" => "$mono_svn/debugger"}, 
		{"mono-addins" => "$mono_svn/mono-addins"}, 
		{"mono-tools" => "$mono_svn/mono-tools"}, 
		{"gtk-sharp" => "$mono_svn/gtk-sharp"}, 
		{"gnome-sharp" => "$mono_svn/gnome-sharp"}, 
		{"monodoc-widgets" => "$mono_svn/monodoc-widgets"}, 
		{"monodevelop" => "$mono_svn/monodevelop"}, 
		{"paint-mono" => "http://paint-mono.googlecode.com/svn/trunk"},
	);

	my (@pkgs);
	foreach my $pkgh (@pkglist) {
		# prep
		my @ks = keys(%$pkgh); my $key = $ks[0];

		# init pkg
		my $pkg = pkg_get($key, $pkgh->{$key});

		# override defaults
		if ($pkg->{name} eq "mcs") {
			delete($pkg->{configurer});
			delete($pkg->{maker});
			delete($pkg->{installer});
		}
		if ($pkg->{name} eq "olive") {
			delete($pkg->{configurer});
			delete($pkg->{maker});
			delete($pkg->{installer});
		}
		if ($pkg->{name} eq "mono") {
			$pkg->{premaker} = "make get-monolite-latest";
		}
		if ($pkg->{name} eq "gtk-sharp") {
			$pkg->{configurer} = "bootstrap-2.14";
		}
		if ($pkg->{name} eq "gnome-sharp") {
			$pkg->{configurer} = "bootstrap-2.24";
		}
		if ($pkg->{name} eq "paint-mono") {
			$pkg->{workdir} = File::Spec->catdir($pkg->{dir}, "src");
		}

		push(@pkgs, $pkg);
	}
	return @pkgs;
}


sub action_list {
	my (@pkgs) = pkglist_get();
	foreach my $pkg (@pkgs) {
		printf("%s\n", $pkg->{name});
	}
}

my %actions = (
	list => -1,
	merge => 0,
	fetch => 1,
	configure => 2,
	make => 3,
	install => 4,
);

sub action_merge {
	my ($action, @worklist) = @_;

	# spit out env.sh to source when running
	env_write();

	# init source dir
	unless (-d $SRCDIR) {
		mkpath($SRCDIR);
	}

	my (@pkgs) = pkglist_get();
	foreach my $pkg (@pkgs) {
		# filter on membership in worklist
		if (grep {$_ eq $pkg->{name}} @worklist) {
			pkg_print($pkg);

			# fetch
			if (($action == $actions{merge}) || ($action == $actions{fetch})) {
				my $revision = "HEAD";
				pkg_fetch($pkg, $revision);
			}

			# configure
			if (($action == $actions{merge}) || ($action == $actions{configure})) {
				pkg_configure($pkg);
			}

			if (($action == $actions{merge}) || ($action == $actions{make})) {
				# premake, if any
				pkg_premake($pkg);

				# make
				pkg_make($pkg);
			}

			# install
			if (($action == $actions{merge}) || ($action == $actions{install})) {
				pkg_install($pkg);
			}
		}
	}
}


sub parse_args {
	if (scalar(@ARGV) == 0) {
		printf("Usage:  %s <action> [<pkg1> <pkg2> | world]\n", $0);
		printf("Actions: %s\n", join(" ", keys(%actions)));
		exit(2);
	}

	my ($action) = $ARGV[0];
	if (!grep {$_ eq $action} keys(%actions)) {
		printf("Invalid action: %s\n", $action);
		exit(2);
	}

	my (@pkgnames) = splice(@ARGV, 1);
	if (grep {$_ eq "world"} @pkgnames) {
		@allpkgs = pkglist_get();
		@pkgnames = ();
		foreach my $pkg (@allpkgs) {
			push(@pkgnames, $pkg->{name});
		}
	}

	return (action => $action, pkgs => \@pkgnames);
}

sub main {
	my (%input) = parse_args();

	printf("Action selected: %s\n", $input{action});
	if (scalar(@{ $input{pkgs} }) > 0) {
		printf("Packages selected:\n");
		foreach my $pkgname (@{ $input{pkgs} }) {
			printf(" * %s\n", $pkgname);
		}
		print("\n");
	}

	if ($actions{$input{action}} == $actions{list}) {
		action_list();
		exit(2);
	}

	action_merge($actions{$input{action}}, @{ $input{pkgs} })
}

main();

Windows Forms: because you're worth it

March 23rd, 2009

Ah, Windows Forms. You've changed my life. I used to think Java was the lowest of low in gui programming, but I've had to reset my lowest common denominator in several areas already. Layout is horrendous, and the WinForms threading model is unfathomable. At least it's nothing important then.

But it also comes wrapped with easter eggs for your enjoyment. Try this on for size:

  1. Create a new thread, run splash screen.
  2. Initialize main gui in default thread.
  3. Signal splash thread to end.
  4. Splash screen calls Close().
  5. Call Join() on splash thread.
  6. Run main gui.
  7. Main gui appears behind all other open windows.

What do you mean that's not what you wanted? So you think to yourself "Aha! I'm cleverer than you are, stupid dot net. I'll make you wish you never gave me a BringToFront() method!" But that, despite the fantastically promising name, doesn't do anything. Neither does Activate().

No big deal. I'm sure noone is planning to use a splash screen in .NET anyway. So after aimlessly looking out the window for an hour and downing another three cups of awful coffee, you snap out of your prolonged daydream and start scrolling through the member list for the seventh time. Hm, a TopMost property. I wonder... Ah yes, that's the Stalin button. Makes your window always on top of all others. But what if..

// <stalin>
this.TopMost = true;
// </stalin>
this.Load += delegate (object o,EventArgs a) { this.TopMost = false; };

Yup, that actually works. When you're initializing the gui you fool the stack that hates you into thinking that you're going to be a dictator, but just when the window loads you turn off god mode. And that's enough to bring it to the front.

Windows Forms is truly fantastic for the kind of people who enjoy trivia. Just remembering hundreds of facts that have no real use just for the sake of knowing what few other people know. Take threading. Now, in most runtimes you would imagine that creating a new thread in your program has semantics such that whatever you've been doing until now in your single threaded application continues to work. The new bit you have to tackle is new threads you create in addition to the main one. Not so in .NET (I bet you saw that coming, you rascal!) For inexplicable reasons, your process has a way of hanging at the end just for the heck of it. I think I figured it out though. Are you ready for it?

splashthread = new Thread(RunSplash);
splashthread.IsBackground = true;
Thread.CurrentThread.IsBackground = true; // cure for cancer?

That's right, suddenly your main thread doesn't work the same anymore. You have to make it a background thread (which it clearly isn't), otherwise it just hangs there after Close().

EDIT: No, that didn't fix it either.

What a shame I didn't discover Windows Forms 7 years ago, I could have spent all that time learning all these exciting hacks instead of wasting my time on useless and unproductive things like Python.

data Binding made of fail?

March 9th, 2009

The following nabbed from csharp-online.net.

using System;
using System.Windows.Forms;

class Student {
	private string name;
	private double gpa;
	
	public string Name {
		get { return name; }
		set { name = value; }
	}
	
	public double Gpa {
		get { return gpa; }
		set { gpa = value; }
	}
}

class Widget : Form
{
	TextBox textBox1;
	TextBox textBox2;
	Button button;
	Student student;
	
	public static void Main(string[] args)
	{
		Application.Run(new Widget());
	}
	
	public Widget()
	{
		student = new Student();
		student.Name = "Jeff";
		student.Gpa = 4.0;

		textBox1 = new TextBox();
		textBox2 = new TextBox();
		textBox1.DataBindings.Add(new Binding("Text", student, "Name"));
		textBox2.DataBindings.Add(new Binding("Text", student, "Gpa"));
		
		button = new Button();
		button.Click += new EventHandler(testButton_Click);
		
		TableLayoutPanel pan = new TableLayoutPanel();
		pan.ColumnCount = 3;
		pan.ColumnCount = 1;
		pan.Controls.Add(textBox1, 0, 0);
		pan.Controls.Add(textBox2, 1, 0);
		pan.Controls.Add(button, 2, 0);
		this.Controls.Add(pan);
	}
	
	private void testButton_Click(object sender, EventArgs e) {
		MessageBox.Show("Name: " + student.Name 
				+ " GPA: " + student.Gpa);
   }
}

Build with:

gmcs binding.cs -r:System.Windows.Forms

Looks tidy, except.. it doesn't do a damn thing. Values are extracted successfully from student, but setting them doesn't do anything. No matter how vigorously I edit the textboxes I always see the same values.

That's on Mono. On .NET I get this evilgram:

See the end of this message for details on invoking 
just-in-time (JIT) debugging instead of this dialog box.

************** Exception Text **************
System.Reflection.TargetInvocationException: Property accessor 'Name' on object 'Student' threw the following exception:'Student.get_Name()' ---> System.MethodAccessException: Student.get_Name() ---> System.Security.SecurityException: Request for the permission of type 'System.Security.Permissions.ReflectionPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
   at System.Security.CodeAccessSecurityEngine.ThrowSecurityException(Assembly asm, PermissionSet granted, PermissionSet refused, RuntimeMethodHandle rmh, SecurityAction action, Object demand, IPermission permThatFailed)
   at System.Security.CodeAccessSecurityEngine.ThrowSecurityException(Object assemblyOrString, PermissionSet granted, PermissionSet refused, RuntimeMethodHandle rmh, SecurityAction action, Object demand, IPermission permThatFailed)
   at System.Security.CodeAccessSecurityEngine.CheckSetHelper(PermissionSet grants, PermissionSet refused, PermissionSet demands, RuntimeMethodHandle rmh, Object assemblyOrString, SecurityAction action, Boolean throwException)
   at System.Security.PermissionSetTriple.CheckSetDemand(PermissionSet demandSet, PermissionSet& alteredDemandset, RuntimeMethodHandle rmh)
   at System.Security.PermissionListSet.CheckSetDemand(PermissionSet pset, RuntimeMethodHandle rmh)
   at System.Security.PermissionListSet.DemandFlagsOrGrantSet(Int32 flags, PermissionSet grantSet)
   at System.Threading.CompressedStack.DemandFlagsOrGrantSet(Int32 flags, PermissionSet grantSet)
   at System.Security.CodeAccessSecurityEngine.ReflectionTargetDemandHelper(Int32 permission, PermissionSet targetGrant, CompressedStack securityContext)
   at System.Security.CodeAccessSecurityEngine.ReflectionTargetDemandHelper(Int32 permission, PermissionSet targetGrant)
The action that failed was:
Demand
The type of the first permission that failed was:
System.Security.Permissions.ReflectionPermission
   --- End of inner exception stack trace ---
   at System.Reflection.MethodBase.PerformSecurityCheck(Object obj, RuntimeMethodHandle method, IntPtr parent, UInt32 invocationFlags)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.ComponentModel.ReflectPropertyDescriptor.GetValue(Object component)
   --- End of inner exception stack trace ---
   at System.ComponentModel.ReflectPropertyDescriptor.GetValue(Object component)
   at System.Windows.Forms.BindToObject.GetValue()
   at System.Windows.Forms.Binding.PushData(Boolean force)
   at System.Windows.Forms.Binding.UpdateIsBinding()
   at System.Windows.Forms.Binding.CheckBinding()
   at System.Windows.Forms.Binding.SetListManager(BindingManagerBase bindingManagerBase)
   at System.Windows.Forms.ListManagerBindingsCollection.AddCore(Binding dataBinding)
   at System.Windows.Forms.BindingsCollection.Add(Binding binding)
   at System.Windows.Forms.BindingContext.UpdateBinding(BindingContext newBindingContext, Binding binding)
   at System.Windows.Forms.Control.UpdateBindings()
   at System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   at System.Windows.Forms.Control.OnParentBindingContextChanged(EventArgs e)
   at System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   at System.Windows.Forms.Control.OnParentBindingContextChanged(EventArgs e)
   at System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   at System.Windows.Forms.Control.set_BindingContextInternal(BindingContext value)
   at System.Windows.Forms.ContainerControl.set_BindingContext(BindingContext value)
   at System.Windows.Forms.ContainerControl.get_BindingContext()
   at System.Windows.Forms.Control.get_BindingContextInternal()
   at System.Windows.Forms.Control.get_BindingContext()
   at System.Windows.Forms.Control.get_BindingContextInternal()
   at System.Windows.Forms.Control.get_BindingContext()
   at System.Windows.Forms.Control.UpdateBindings()
   at System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   at System.Windows.Forms.Control.OnParentBindingContextChanged(EventArgs e)
   at System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   at System.Windows.Forms.Control.OnParentBindingContextChanged(EventArgs e)
   at System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   at System.Windows.Forms.ContainerControl.OnCreateControl()
   at System.Windows.Forms.Form.OnCreateControl()
   at System.Windows.Forms.Control.CreateControl(Boolean fIgnoreVisible)
   at System.Windows.Forms.Control.CreateControl()
   at System.Windows.Forms.Control.WmShowWindow(Message& m)
   at System.Windows.Forms.Control.WndProc(Message& m)
   at System.Windows.Forms.ScrollableControl.WndProc(Message& m)
   at System.Windows.Forms.ContainerControl.WndProc(Message& m)
   at System.Windows.Forms.Form.WmShowWindow(Message& m)
   at System.Windows.Forms.Form.WndProc(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)


************** Loaded Assemblies **************
mscorlib
    Assembly Version: 2.0.0.0
    Win32 Version: 2.0.50727.1433 (REDBITS.050727-1400)
    CodeBase: file:///C:/WINDOWS/Microsoft.NET/Framework/v2.0.50727/mscorlib.dll
----------------------------------------
Binding
    Assembly Version: 0.0.0.0
    Win32 Version:  
    CodeBase: file:///Z:/lumiere/Binding.exe
----------------------------------------
System.Windows.Forms
    Assembly Version: 2.0.0.0
    Win32 Version: 2.0.50727.1433 (REDBITS.050727-1400)
    CodeBase: file:///C:/WINDOWS/assembly/GAC_MSIL/System.Windows.Forms/2.0.0.0__b77a5c561934e089/System.Windows.Forms.dll
----------------------------------------
System
    Assembly Version: 2.0.0.0
    Win32 Version: 2.0.50727.1433 (REDBITS.050727-1400)
    CodeBase: file:///C:/WINDOWS/assembly/GAC_MSIL/System/2.0.0.0__b77a5c561934e089/System.dll
----------------------------------------
System.Drawing
    Assembly Version: 2.0.0.0
    Win32 Version: 2.0.50727.1433 (REDBITS.050727-1400)
    CodeBase: file:///C:/WINDOWS/assembly/GAC_MSIL/System.Drawing/2.0.0.0__b03f5f7f11d50a3a/System.Drawing.dll
----------------------------------------
Accessibility
    Assembly Version: 2.0.0.0
    Win32 Version: 2.0.50727.1433 (REDBITS.050727-1400)
    CodeBase: file:///C:/WINDOWS/assembly/GAC_MSIL/Accessibility/2.0.0.0__b03f5f7f11d50a3a/Accessibility.dll
----------------------------------------

************** JIT Debugging **************
To enable just-in-time (JIT) debugging, the .config file for this
application or computer (machine.config) must have the
jitDebugging value set in the system.windows.forms section.
The application must also be compiled with debugging
enabled.

For example:


    


When JIT debugging is enabled, any unhandled exception
will be sent to the JIT debugger registered on the computer
rather than be handled by this dialog box.