Wednesday, February 07, 2007

Some days I really want to strangle Microsoft engineers.

 

For the past couple days I've been working on a project where I want a CE device to dial in to an XP desktop. In a perfect world, I'd just use RAS, create an entry, and go.  Well this isn't a perfect world.

 

Spent several hours yesterday trying different chipsets and PCs to finally determine that my device modem seems to be not-so-good and it won't negotiate at the default 56kbps that the chipset uses (than you Alex Feinman for the help).

 

The workaround is to manually slow it down with an AT command.  Using terminal on the device I just type it in and boom - it's set.  Next step was to try to repro that simple operation in code.  This is where the fun begins. Well this extra dialing data is set via the RasSetEntryProperties API in the lpb parameter.  The parameter docs say "Pointer to a buffer that contains device-specific configuration information. This is opaque TAPI device configuration information." 

 

It gives a reference to the TAPI lineGetDevConfig API for getting that data.  So we look over there and fine that it's a VARSTRING parameter.  Simple enough eh?  I fill in a VARSTRING with the command and send it in.  RasSetEntryProperties returns a success.

 

For fun I then open the connection via the UI's Network Connections.  Click on the "Configure" button and the shell completely locks up.  That can't be good.

 

So I figure, I'll see what the UI generates and use that to figure out what I screwed up.  I create a connection entry manually and add the info into the dialog.  Now clicking "Configure" works fine, so it's back to the code.

 

I call RasGetEntryProperties on the manually created entry to see what it looks like.  I see my data's in there, but it certainly isn't aligned as a VARSTRING and I have no idea what the "header" data is.  The length reported is also *way* bigger than my data.

 

  0x001291C0  30 00 00 00 78 00 00 00 10 01 00 00 00 4b 00 00  0...x........K..

  0x001291D0  00 00 08 00 00 00 61 00 74 00 2b 00 6d 00 73 00  ......a.t.+.m.s.

  0x001291E0  3d 00 76 00 33 00 32 00 2c 00 2c 00 31 00 34 00  =.v.3.2.,.,.1.4.

  0x001291F0  34 00 30 00 30 00 2c 00 32 00 38 00 38 00 30 00  4.0.0.,.2.8.8.0.

  0x00129200  30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  0...............

  etc.

 

Alright, so what's going on here?  Off to the Platform Builder source (and this is why every developer should get the PB eval and install it - the source is gold) to see how the default UI is doing it.  A little GREP on the dialog text and I find that the UI handling this is, not surprisingly NETUI.

 

So after a long while tracing through convoluted code (Source Insight is an invaluable tool here) it appears that you're supposed to call lineConfigDialogEdit and have TAPI give a dialog to fill this structure.  Well what if you don't want user interaction (like in my case where the user would have no idea what to enter)?  Or even more fun, what if it's a headless device?

 

Ok, so time to regroup.  Let's search all the PB source for anything calling lineSetDevConfig to see if there's something to base my work on.  Turns out that there are a few samples, but most of them just do a lineGetDevConfig, save that data somewhere, then send it back in - they don't actually modify it.

 

One exception is the sample RASENTRY app.  It calls lineGetDevConfig, and then appears to alter that config not through lineSetDevConfig but instead by calling lineDevSpecific.  It uses this to change the baud rate.  It's not exactly what I need (I need to prevent the modem from negotiating a v.90 or v.92 connection) but a start, right?

 

So a little work and I ended up with this (well more than this, but it's the heart of it):

 

            result = lineGetDevConfig(tapiId, config, DEV_CLASS_COMM_DATAMODEM);

            ucd.dwCommand = UNIMDM_CMD_CHG_DEVCFG;

            ucd.lpszDeviceClass = DEV_CLASS_COMM_DATAMODEM;

            ucd.lpDevConfig = config;

            ucd.dwOption = UNIMDM_OPT_CFGBLOB;

            ucd.dwValue = (DWORD)MY_CUSTOM_DIALUP_STRING;

            result = lineDevSpecific(hLine, 0, NULL, &ucd, sizeof(ucd));

 

I run it and all APIs return success.  Hooray!  Right?  Open the connection with the UI on the device and the "Extra Settings" box is still blank.  At this point I have to chalk it up to a huge WTF in the Microsoft code.

 

At this point I have no option but to go the route that the UNIMODEM dialer went (again in PB source) and set it in the registry at HKEY_LOCAL_MACHINE\Drivers\Unimodem\Init.  Some may argue that this is easier and I should have gone that route in the first place, but that just doesn't sit well with me and not just because it affects all Unimodem connections on the device. 

 

I strive to understand what is going on when I'm writing code because it not only makes it easier to debug aberrant behavior but it also helps make me a better developer all around. I hate black-box programming - that is programming where I don't fully understand how something works from end to end.  Somehow TAPI gets that data into the blob returned by RasGetEntryProperties, but I'll be damned if I can figure it out. 

 

The actual blob is created or copied to  HKCU\Comm\RasBook\<connection name>\DevCfg and it appears to be an extension of HKLM\Drivers\Unimodem\DevConfig, but they are slightly different and what those differences mean is totally undocumented.

2/7/2007 3:18:21 PM (Eastern Standard Time, UTC-05:00)  #    Comments [1]  | 
 Saturday, January 13, 2007

A guy in the newsgroups who is new to .NET, coming from VB6, is having a hard time drawing on a Form.  The problem with VB6 was it had the Shape controls which provided a crutch for a developer to never actually understand how drawing worked, and now that crutch has been removed.

Drawing is simple - you need a Graphics object to paint on.  The easiest way to get that for your Form is to just override OnPaint - you'll get it as an input parameter.  The other nice effect is that you don't have to do anything other than refresh the Form to get your code to run.

Now normally I don't like to just give out the answer - no one learns much that way - but he seems to genuinely have spent several hours trying to get this, so I figured I'd throw him a bone so he doesn't get frustrated and give up altogether.  So the goal is to draw a "crosshair" on the Form that the user can move around with the D-Pad on the device.  Again, this is a very, very basic example - it took me roughly 15 minutes to do (and that's becasue my VB is very rusty).

Create a device WinForms app.  Select the Form and make sure KeyPreview is True.  Then add this code:

Public Class Form1
   Private crosshairs As New Rectangle(0, 0, 20, 20)
   Private bluePen As New Pen(Color.Blue)
   Private Const StepBy As Int32 = 3

Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
   Dim b As New SolidBrush(Color.Blue)

   ' let the system do it's normal drawing
   MyBase.OnPaint(e)

   'now draw our crosshairs
   DrawCrosshairs(e.Graphics)
End Sub

Private Sub DrawCrosshairs(ByRef g As Graphics)
   g.DrawEllipse(bluePen, crosshairs)
   g.DrawLine( bluePen, _
               crosshairs.Left + crosshairs.Width / 2, _
               
crosshairs.Top - 10, _
               crosshairs.Left + crosshairs.Width / 2, _
               crosshairs.Bottom + 10)
   g.DrawLine( bluePen, _
               crosshairs.Left - 10, _
               crosshairs.Top + crosshairs.Height / 2, _
               crosshairs.Right + 10, _
               crosshairs.Top + crosshairs.Height / 2)
End Sub

Private Sub Form1_KeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown
   If (e.KeyCode = System.Windows.Forms.Keys.Up) Then
      If crosshairs.Top >= StepBy Then
         crosshairs.Offset(0, -StepBy)
      End If
   End If
   If (e.KeyCode = System.Windows.Forms.Keys.Down) Then
      If crosshairs.Bottom <= Me.Height - StepBy Then
         crosshairs.Offset(0, StepBy)
     End If
   End If
   If (e.KeyCode = System.Windows.Forms.Keys.Left) Then
      If crosshairs.Left >= StepBy Then
         crosshairs.Offset(-StepBy, 0)
      End If
   End If
   If (e.KeyCode = System.Windows.Forms.Keys.Right) Then
      If crosshairs.Right <= Me.Width - StepBy Then
         crosshairs.Offset(StepBy, 0)
      End If
   End If
   'this forces OnPaint to be called
   Me.Refresh()
End Sub
End Class

That's all there is to it.

1/13/2007 6:18:19 PM (Eastern Standard Time, UTC-05:00)  #    Comments [1]  | 
 Friday, January 12, 2007

This question comes up a lot, and the well-known and documented workaround is to set your Form's Text property to an empty string to hide it from the Running Programs applet so I won't go into that.  And before reading further, if you're after hiding a Form, then go Google for that info - everything I present here are ways that are proven to not work.  It's simply info for those who are tempted to try another route.

We know that the Running Programs applet simply enumerates all top-level windows with text.  We also know that a lot of native apps don't have the problem of child windows showing up, so I decided to try to mimic that behavior with some P/Invoke shenanigans.  The test app simply had 2 forms - Form1 would create a Form2 instance on a button click and then call ShowDialog on the new Form2 instance.  After that (if it got that far), I'd check the applet to see if my code worked.

Method 1: Reparent the Form with SetParent

[DllImport("coredll.dll", SetLastError = true)]
internal static extern IntPtr SetParent(IntPtr hwndChild, IntPtr hwndNewParent);

  • Tried calling this after creating the Form2 and before calling ShowDialog.  ShowDialog subsequently throws an ArgumentException
  • Tried calling it in the ctor of Form2. ShowDialog subsequently throws an ArgumentException
  • Tried passing Form1's Handle to Form 2, then calling SetParent in a Form2.Activate handler. ShowDialog subsequently throws an ArgumentException
  • Tried calling SetParent from a button click in Form2, so after the dialog is loaded and shown.  No exception now, but when SetParent runs, Form1 gains scrollbars, Form2's controls end up in Form1 along with Form1's controls (so a mash up of both) and all controls are non-responsive.

Method 2: Change the Window Style bits

internal const int GWL_STYLE = -16;
internal const int GWL_EXSTYLE = -20;
internal const uint WS_CHILD = 0x40000000;
internal const uint WS_EX_APPWINDOW = 0x00040000;

[DllImport("coredll.dll", SetLastError = true)]
internal static extern uint SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);

[DllImport("coredll.dll", SetLastError = true)]
internal static extern uint GetWindowLong(IntPtr hWnd, int nIndex);

  • Tried setting style to WS_CHILD after creation and before ShowDialog.  Still appears in applet.
  • Tried setting style to WS_CHILD in Form2 ctor.  Still appears in applet.
  • Tried setting style to WS_CHILD in Form2 Activate handler.  Still appears in applet.
  • Tried setting style to WS_CHILD in Form2 button click handler.  Still appears in applet.
  • Tried unsetting extended style to WS_EX_APPWINDOW after creation and before ShowDialog.  Still appears in applet.
  • Tried unsetting extended style to WS_EX_APPWINDOW in Form2 button handler.  Still appears in applet.

So what's the take away lesson?  CF Forms like to be shown in that damned applet, and the simplest mechanism is still to just set the text to an empty string when you want to hide it.

1/12/2007 2:10:24 PM (Eastern Standard Time, UTC-05:00)  #    Comments [1]  | 

When we use a language, often we tend to overlook some of the more obvious constructs or be frustrated by what we think should work.  For example, assume we have this simple problem - our function receives a value and based on that value we run through a switch, but we have code that will be run for multiple cases.  Explaining what I mean in words is tough - so let's look at what I'm trying to say in code.  Assume we have these enums:

[Flags]
enum Foo
{
  NoFoo = 0,
  FooA = 1,
  FooB = 2,
  FooC = 4
}

enum Bar
{
  A,
  B,
  C
}

We want a function that will take in a Bar, and based on that create a Foo.  If Bar is A, the the resulting Foo is a combination of FooA, FooB and FooC.  If Bar is B, then it's a combination of FooB and FooC.  If Bar is C, then the result is just FooC.  In C, we'd just do this:

Foo FooBar(Bar bar)
{
  Foo foo = Foo.NoFoo;

  switch (bar)
  {
    case Bar.A:
      foo |= Foo.FooA;
    case Bar.B:
      foo |= Foo.FooB;
    case Bar.C:
      foo |= Foo.FooC;
    break;
  }
  return foo;
}

Letting each case fall into the next intentionally. Yes it's a contrived example, but you get the idea.  There are cases when we need to do processing like this (like a project I'm doing right now).

Well C# doesn't like this type of construct - I'm not certain why it's illegal (other than missing breaks are common bugs) - but the compiler will say 'Error: Control cannot fall through from one case label ('case 0:') to another'.  So you might code a 'fix' like this:

Foo FooBar(Bar bar)
{
   Foo foo = Foo.NoFoo;

   switch (bar)
   {
      case Bar.A:
         foo |= Foo.FooA;
      break;
      case Bar.B:
         foo |= Foo.FooA;
         foo |= Foo.FooB;
      break;
      case Bar.C:
         foo |= Foo.FooA;
         foo |= Foo.FooB;
         foo |= Foo.FooC;
      break;
   }
   return foo;
}

Not too bad, but if you have to do more processing than a single line it gets ugly and maintainability goes downhill fast.

The thing to keep in mind in C# is that those case statements are actually labels, so you can use them as such, meaning they are valid goto targets, so this code is perfectly valid:

Foo FooBar3(Bar bar)
{
   Foo foo = Foo.NoFoo;

   switch (bar)
   {
      case Bar.A:
         foo |= Foo.FooA;
         goto case Bar.B;
      case Bar.B:
         foo |= Foo.FooB;
         goto case Bar.C;
      case Bar.C:
         foo |= Foo.FooC;
      break;
   }
   return foo;
}

1/12/2007 12:34:40 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Wednesday, January 10, 2007

Bugs are frustrating, but when you write code for a living you expect them and live with them as a fact of life.  They are a lot harder to swallow when they are introduced by other libraries, especially when those libraries work counter to what you would expect.  It's even more frustrating when it's in a library like the Compact Framework itself.

Recently we got a bug report for our ConnectionManager in some code that I know we tested - basically the Description returned by the DestinationInfo class was returning odd data and concatenating to it did bad things.  Experience told me that it sounded like the data coming back from the native API was not getting properly truncated at a NULL.  Not an unusual mistake, so I went to find it.

Description = Marshal2.PtrToStringUni(baseAddr, 16, 256);
int nullPos = Description.IndexOf('\0'
);
if (nullPos > -1) Description = Description.Substring(0, nullPos);

The bad news was the code looked right - we were looking for NULL and trimming at it.  The only way a problem could arise is if the buffer was non-zero at the start.  So off to look at that code.  Here's where the allocation is made:

hDestInfo = Marshal.AllocHGlobal(DestinationInfo.NativeSize);

Again, it looked right.  However, talking with Alex Feinman he said that the Marshal Alloc functions do no zero memory.  In the earlier versions of this code we used our internal MarshalEx, which P/Invoked LocalAlloc with the LPTR parameter, which zeros everything at allocation.  Why the hell the CF doesn't do that one can only guess, but it makes no sense in my book.  Who would want to allocate memory and not zero it?  Worse still is that MSDN doesn't say that the memory is not zeroed.

So then, changing from our MarshalEx function to the CF's Marshal function introduced an error.  So how do we fix it?  There's no CF equivalent to a memset (again, this is a WTF in my book, but there are a few language limitations like this that irritate me - try taking an IntPtr and turning the data at that location into a managed struct without having to copy it).

So anyway, I looked at coredll.def from Platform Builder 5.0 to see if coredll.dll helps, and sure enough I see this:

malloc @1041
calloc @1346
_memccpy @1042
memcmp @1043
memcpy @1044
_memicmp @1045
memmove @1046
memset @1047

P/Invoke to the rescue.  I added the following to the SDF (so it will be in the next release).  Note that wile I was there I handles the annoyances of not having a way to copy from an IntPtr to an IntPtr (memcpy) or a way to validate IntPtrs (IsBad[Read/Write]Ptr) while I was at it.

public static void SetMemory(IntPtr destination, byte value, int length)
public static void SetMemory(IntPtr destination, byte value, int length, bool boundsCheck)
public static void Copy(IntPtr source, IntPtr destination, int length)
public static void Copy(IntPtr source, IntPtr destination, int length, bool boundsCheck)
public static bool IsSafeToWrite(IntPtr destination, int length)
public static bool IsSafeToRead(IntPtr source, int length)

Then a simple fix back in the ConnectionManager:

hDestInfo = Marshal.AllocHGlobal(DestinationInfo.NativeSize);
Marshal2.SetMemory(hDestInfo, 0, DestinationInfo.NativeSize, false);

 

1/10/2007 9:51:46 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]  | 
 Tuesday, January 09, 2007

So this morning we got an email from PayPal:

The PayPal User Agreement states that PayPal, at its sole discretion, reserves the right to limit an account for any violation of the User Agreement, including the Acceptable Use Policy. Under the Acceptable Use Policy, PayPal may not be used to send or receive payments or donations for obscene or certain sexually oriented goods or services. The complete Acceptable Use Policy addressing Mature Audiences can be found at the following URL:

http://www.paypal.com/cgi-bin/webscr?cmd=p/gen/ua/use/index_frame-outside&ed=mature

We are hereby notifying you that, after a recent review of your account activity, it has been determined that you are in violation of PayPal's Acceptable Use Policy regarding your website:

http://www.opennetcf.org/forums/topic.asp?TOPIC_ID=8609. Therefore, your account has been permanently limited.

If you have a remaining balance, you may withdraw the funds to your bank account. Information on how to withdraw funds from your PayPal Account can be found at our Help Center.

You will need to remove all references to PayPal from your website(s) and/or auction(s). This includes not only removing PayPal as a payment option, but also the PayPal logo and/or shopping cart. We thank you in advance for your cooperation. If you have any questions, please contact the PayPal Acceptable Use Policy Department at aup@paypal.com.

Sincerely,

PayPal Acceptable Use Policy Department PayPal, an eBay Company

Right off you can tell that they didn't like something in our public Forums - a place where anyone who can enter a user name can post anything they'd like.  Offhand I don't even know what was at that topic ID as Neil deleted it once he saw this notification.  We also use the PayPal account very, very rarely - I'd guess there were probably 5 transactions in the last 12 months, so how the hell they figured some violation by "reviewing account activity" is beyond my comprehension.

So Neil replid to them that we had no activity of the sort and that the material was removed.  Shortly after that we got this:

Dear Chris Tacke,

Based on the information provided to you in our last email your account has been permanently closed. Under the Acceptable Use Policy, PayPal may not be used to send or receive payments or donations for obscene or certain sexually oriented goods or services. The complete Acceptable Use Policy addressing Mature Audiences can be found at the following URL:

http://www.paypal.com/cgi-bin/webscr?cmd=p/gen/ua/use/index_frame-outside&ed=mature

Unfortunately, we will be unable to overturn the limitation on your account. I do apologize for any inconvenience this issue may be causing you at this time.

Sincerely,
PayPal Acceptable Use Policy Department
PayPal an eBay Company

Nice.  One "infraction" of material, which we didn't post (again spammers are the lowest form of life) and which we removed, and the close the account with really no appeals process.  Not only do they charge fees way above what any other merchant provider charges and not only do they tend to monopolize eBay commerce, they evidently also employ a large number of morons. I like how they term "permanently closed" as a "limitation on your account."

How does a company like this stay in business?

1/9/2007 11:13:53 AM (Eastern Standard Time, UTC-05:00)  #    Comments [1]  |