Dear Sybase,

Thank you for providing the development community with a free edition of your SQL Anywhere database. We also appreciate the work you put in to an ADO.NET DataProvider so that we can use SQL Anywhere from .NET. Now I'm not the type of guy who complains about free things, so please think of this as constructive criticism.

As some background, I work on BuildMaster and needed to write a database provider for SQL Anywhere. Thanks to your easy-to-use API, things went along pretty smoothly. Well... until there was an error.

If that error box looks familiar, it's probably because it came from your library. Overlooking the fact that dblgen12.dll was actually sitting in the working directory — or the fact that this is thrown from an impossible to try-catch static constuctor — please consider that modal dialogs aren't a very good way to convey error messages.

Sure, most users won't complain when a meaningless message pops up, but if there's no user to click "OK" — say, when your library is being used on a web server — that creates a bit of a problem. Namely, the entire process halts until a non-existent user clicks on a never-seen message box.

While I appreciate that this answers the long-debated philosophical question, if a MessageBox Shows and no user is there to click it, does it still appear, I would imagine that others might not appreciate such subtle nuances.

The good news is, I've figured out exactly how you can fix this! While trudging around in Reflector to debug this awry UI, I noticed that the managed iAnywhere.Data.SQLAnywhere.dll library references two unmanaged libraries: dbdata12.dll and dblgen12.dll. As I'm sure you already know, you're actually embedding dbdata12.dll (both the 32- and 64-bit versions) in your main library and loading it as needed.

It's actually a pretty clever technique, which you obviously already knew since you wrote the code. All you really need to do now is write a function called CreateTheOtherDLL() and put it in LoadDll(). I took the liberty of showing you exactly where you should call this other function in your code.

Obviously, I don't have your actual source code, but I was able to use the handy-dandy Reflector to disassemble your library into C#. I also took the liberty of adding some comments in, since I wasn't sure how well-commented your code is; I'm a pretty good commenter, so feel free to add any of this stuff in.

/// <summary>
/// Writes out the embedded dbdata.dll library and then tries to load it
/// </summary>
private static void LoadDll()
{
    bool success = false;

    // Build an array of:
    //   1. System temporary directory 
    //   2. Current working directory
    //   3. SQL Anywhere .NET Data Provider assembly directory
    string[] directoriesToUse = new string[] { 
        // Let's trim everything, just to be extra safe!
        Path.GetTempPath().Trim(),
        Directory.GetCurrentDirectory().Trim(), 
        Path.GetDirectoryName(GetLocationFromCodeBase().Trim()) };

    // loop over each directory in the array
    for (int i = 0; !success && (i < directoriesToUse.GetLength(0)); i++)
    {
        // alternate directory (magic explained below)
        int directorySubIndex = 0;

        // grab the last character from the directory name
        char maybeASlash = directoriesToUse[i][directoriesToUse[i].Length - 1];

        // make a pretty-damn unique directory (PDUD) for dbdata.dll
        string basePath = string.Format("{0}{1}{2}_", 
            directoriesToUse[i], 

            // we want to make sure that when doing a Path Combine,
            // there is a slash in the middle
            ((maybeASlash == '\\') || (maybeASlash == '/')) ? "" : @"\", 

            // GUID generated 2009-08-24; could not find anyone else using it
            "{16AA8FB8-4A98-4757-B7A5-0FF22C0A6E33}"); 

        try
        {
            // it's possible that our PDUD (see above) is being used by 
            // something else, so we'll just keep incrementing directorySubIndex
            // until we find an unused one; maybe directorySubIndex should have
            // been a long, just to be safe?
            while (!success)
            {
                // set the *final* path of dbdata.dll
                string realBasePath = basePath + directorySubIndex.ToString();
                this.s_dllPath = realBasePath + @"\dbdata.dll"; 

                // hopefully, this won't exist yet!
                if (!Directory.Exists(realBasePath))
                {
                    Directory.CreateDirectory(realBasePath);
                    CreateDll(); 
                   /*** ADD A CALL TO CreateTheOtherDLL() HERE ***/                    success = true;
                }
                else
                {
                    // in theory, we should be able to just delete the existing dll
                  // and then recreate it
                  if (!File.Exists(this.s_dllPath) || TryDeleteFile(this.s_dllPath))
                    {
                        CreateDll();

                        // neat combo to break out of a loop; note how success 
                        // is tested in the for loop above
                        success = true; 
                        continue;
                    }
                    directorySubIndex++;
                }
            }
        }
        catch (Exception)
        {
           // meh, probably wasn't important anyway
        }
    }


    if (!success)
    {
        ThrowException(string.Format(
            "SQL Anywhere .NET Data Provider requires access "
            + "permissions of one of the following directories:\r\n"
            + "1. System temporary directory ({0})\r\n"
            + "2. Current working directory ({1})\r\n"
            + "3. SQL Anywhere .NET Data Provider assembly directory ({2})"
            , directoriesToUse[0], directoriesToUse[1], directoriesToUse[2]));
    }

    
    this.s_hModule = PInvokeMethods.LoadLibrary(s_dllPath);
    if (this.s_hModule == IntPtr.Zero)
    {
        ThrowException(string.Format(
          "Failed to load native dll ({0}).", 
          this.s_dllPath));
    }
}


private static bool TryDeleteFile(string path)
{
    bool reallyDeleted = false;
    try
    {
        File.Delete(path);
        reallyDeleted = !File.Exists(path); // can never be to sure
    }
    catch (Exception)
    {
       // note the method name... Try not Catch!
    }
    return reallyDeleted;
}

See? It should be pretty easy.

Anyway, I hope this constructive critisism was helpful. I really do appreciate the free version of SQL Anywhere, and I just want to help you make it more awesome.

 

Warm Regards,

Alex
 

P.S. In case you were worried about the Message Box getting in the way of what I was doing, one of my coworkers came up with a fantastic workaround: use the Windows Computer Based Training hooks to quash unexpected UI from popping up.

P.P.S. No, this isn't the worse thing I've ever seen.

P.P.P.S. I should also note that I'm not a fan of open letters. I think they're pompous, mean-spirited, and completely unproductive. However, it's my hope that by hanging a lantern on this fact (and the fact that I'm hanging a lantern on hanging a lantern (and the fact that I'm hanging a lantern on hanging a lantern on hanging a lantern (ad nfinitum))) in the post-post post-script, you will become sufficiently distracted and appreciate the device.