An updated version of RegDL available

As mentioned in the last post, I decided to update the RegDL tool – which can be used to create demand-loading entries for an AutoCAD .NET module from, for instance, an installer – to support optional logging to a file. If you now run RegDL.exe with the /log command-line switch, then the application will now create a text file, reglog.txt, in the executable's folder with the high-level results of the registration (with hopefully some useful detail in case of failure).

Here are the main, updated files, Program.cs:

using System.Reflection;

using System.IO;

using System;

using DemandLoading;

 

namespace RegDL

{

  class Program

  {

    static void Main(string[] args)

    {

      // We need at least one argument (the assembly name)

      // or the request for help

 

      if (args.Length <= 0 ||

          args.Length == 1 &&

          (args[0] == "/?" || args[0].ToLower() == "/help"))

      {

        PrintUsage();

        return;

      }

 

      // Get the first argument and check the file exists

 

      string asmName = args[0];

      if (!File.Exists(asmName))

      {

        Log(

          String.Format(

            "RegDL : Unable to locate input assembly '{0}'.",

            asmName

          ),

          false

        );

        return;

      }

 

      // Now we get the optional flags

 

      bool startup = false;

      bool hklm = false;

      bool unreg = false;

      bool force = false;

      bool log = false;

 

      for (int i=1; i < args.Length; i++)

      {

        string arg = args[i].ToLower();

        startup |= (arg == "/startup");

        hklm |= (arg == "/hklm");

        unreg |= (arg == "/unregister");

        force |= (arg == "/force");

        log |= (arg == "/log");

      }

 

      // As each dependent assembly is resolved, we need to make

      // sure it is loaded via Reflection-Only, not a full Load

 

      AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve +=

        delegate(object sender, ResolveEventArgs rea)

        {

          return Assembly.ReflectionOnlyLoad(rea.Name);

        };

 

      // Let's load our assembly via Reflection-Only

 

      Assembly assem =

        Assembly.ReflectionOnlyLoadFrom(asmName);

 

      bool res = false;

 

      if (unreg)

      {

        // Unregister the assembly, if possible

 

        try

        {

          res = RegistryUpdate.UnregisterForDemandLoading(assem);

        }

        catch(Exception ex)

        {

          Log(String.Format("Exception: {0}", ex), log);

        }

 

        if (res)

        {

          Log(

            String.Format(

              "Removed demand-loading information for '{0}'.",

              asmName

            ),

            log

          );

        }

        else

        {

          Log(

            String.Format(

            "Could not remove demand-loading information for '{0}'.",

              asmName

            ),

            log

          );

        }

      }

      else

      {

        // Register the assembly, if possible

 

        try

        {

          res =

            RegistryUpdate.RegisterForDemandLoading(

              assem, !hklm, startup, force

          );

        }

        catch(Exception ex)

        {

          if (ex is ReflectionTypeLoadException)

          {

            Log(

              "Trouble loading types: do you have AcMgd.dll and " +

              "AcDbMgd.dll in the same folder as RegDL.exe?", log

            );

          }

          else

          {

            Console.WriteLine("Exception: {0}", ex);

          }

        }

 

        if (res)

        {

          Log(

            String.Format(

            "Registered assembly '{0}' for AutoCAD demand-loading.",

              asmName

            ),

            log

          );

        }

        else

        {

          Log(

            String.Format(

              "Could not register '{0}' for AutoCAD demand-loading.",

              asmName

            ),

            log

          );

        }

      }

    }

 

    // Print a usage message

 

    static void PrintUsage()

    {

      const string indent = "    ";

      const string start = "Version=";

 

      string version =

        Assembly.GetExecutingAssembly().FullName;

 

      if (version.Contains(start))

      {

        // Get the string starting with the version number

 

        version =

          version.Substring(

            version.IndexOf(start) + start.Length

          );

 

        // Strip off anything after (and including) the comma

 

        version =

          version.Remove(version.IndexOf(','));

      }

      else

        version = "";

 

      Console.WriteLine(

        "AutoCAD .NET Assembly Demand-Loading Registration " +

        "Utility {0}", version

      );

      Console.WriteLine(

        "Written by Kean Walmsley, Autodesk."

      );

      Console.WriteLine(

        "http://blogs.autodesk.com/through-the-interface"

      );

      Console.WriteLine();

      Console.WriteLine("Syntax: RegDL AssemblyName [Options]");

      Console.WriteLine("Options:");

      Console.WriteLine(

        indent +

        "/unregister  Remove demand-loading keys for this assembly"

      );

      Console.WriteLine(

        indent +

        "/hklm        Write keys under HKLM rather than HKCU"

      );

      Console.WriteLine(

        indent +

        "/startup    Assembly to be loaded on AutoCAD startup"

      );

      Console.WriteLine(

        indent +

        "/force      Overwrite keys, should they already exist"

      );

      Console.WriteLine(

        indent +

        "/log        All output goes to a log file"

      );

      Console.WriteLine(

        indent +

        "/? or /help  Display this usage message"

      );

    }

 

    static internal void Log(string str, bool toFile)

    {

      if (toFile)

      {

        Assembly asm = Assembly.GetExecutingAssembly();

        string path = Path.GetDirectoryName(asm.Location);

        string logfile = path + "\\reglog.txt";

 

        // Create a writer and open the file

 

        StreamWriter log;

 

        if (!File.Exists(logfile))

        {

          log = new StreamWriter(logfile);

        }

        else

        {

          log = File.AppendText(logfile);

        }

 

        // Write to the file:

 

        log.WriteLine(DateTime.Now);

        log.WriteLine(str);

        log.WriteLine();

 

        // Close the stream:

        log.Close();

      }

      else

      {

        Console.WriteLine(str);

      }

    }

  }

}

And demand-loading-external.cs:

using System.Collections.Generic;

using System.Reflection;

using System.Resources;

using System;

using Microsoft.Win32;

 

namespace DemandLoading

{

  public class RegistryUpdate

  {

    public static bool RegisterForDemandLoading(

      Assembly assem, bool currentUser, bool startup, bool force

    )

    {

      // Get the assembly, its name and location

 

      string name = assem.GetName().Name;

      string path = assem.Location;

 

      // We'll collect information on the commands

      // (we could have used a map or a more complex

      // container for the global and localized names

      // - the assumption is we will have an equal

      // number of each with possibly fewer groups)

 

      List<string> globCmds = new List<string>();

      List<string> locCmds = new List<string>();

      List<string> groups = new List<string>();

 

      // Iterate through the modules in the assembly

 

      Module[] mods = assem.GetModules(true);

      foreach (Module mod in mods)

      {

        // Within each module, iterate through the types

 

        Type[] types = mod.GetTypes();

 

        foreach (Type type in types)

        {

          // We may need to get a type's resources

 

          ResourceManager rm =

            new ResourceManager(type.FullName, assem);

          rm.IgnoreCase = true;

 

          // Get each method on a type

 

          MethodInfo[] meths = type.GetMethods();

          foreach (MethodInfo meth in meths)

          {

            // Get the method's custom attribute(s)

 

            IList<CustomAttributeData> attbs =

              CustomAttributeData.GetCustomAttributes(meth);

 

            foreach (CustomAttributeData attb in attbs)

            {

              // We only care about our specific attribute type

 

              if (attb.Constructor.DeclaringType.Name ==

                  "CommandMethodAttribute")

              {

                // Harvest the information about each command

 

                string grpName = "";;

                string globName = "";

                string locName = "";

                string lid = "";

 

                // Our processing will depend on the number of

                // parameters passed into the constructor

 

                int paramCount = attb.ConstructorArguments.Count;

 

                if (paramCount == 1 || paramCount == 2)

                {

                  // Constructor options here are:

 

                  //  globName (1 argument)

                  //  grpName, globName (2 args)

 

                  globName =

                    attb.ConstructorArguments[0].ToString();

                  locName = globName;

                }

                else if (paramCount >= 3)

                {

                  // Constructor options here are:

 

                  //  grpName, globName, flags (3 args)

                  //  grpName, globName, locNameId, flags (4 args)

                  //  grpName, globName, locNameId, flags,

                  //    hlpTopic (5 args)

                  //  grpName, globName, locNameId, flags,

                  //    contextMenuType (5 args)

                  //  grpName, globName, locNameId, flags,

                  //    contextMenuType, hlpFile, helpTpic (7 args)

 

                  CustomAttributeTypedArgument arg0, arg1;

                  arg0 = attb.ConstructorArguments[0];

                  arg1 = attb.ConstructorArguments[1];

 

                  // All options start with grpName, globName

 

                  grpName = arg0.Value as string;

                  globName = arg1.Value as string;

                  locName = globName;

 

                  // If we have a localized command ID,

                  // let's look it up in our resources

 

                  if (paramCount >= 4)

                  {

                    // Get the localized string ID

 

                    lid = attb.ConstructorArguments[2].ToString();

 

                    // Strip off the enclosing quotation marks

 

                    if (lid != null && lid.Length > 2)

                      lid = lid.Substring(1, lid.Length - 2);

 

                    // Let's put a try-catch block around this

                    // Failure just means we use the global

                    // name twice (the default)

 

                    if (lid != null && lid != "")

                    {

                      try

                      {

                        locName = rm.GetString(lid);

                      }

                      catch

                      { }

                    }

                  }

                }

 

                if (globName != null)

                {

                  // Add the information to our data structures

 

                  globCmds.Add(globName);

                  locCmds.Add(locName);

 

                  if (grpName != null && !groups.Contains(grpName))

                    groups.Add(grpName);

                }

              }

            }

          }

        }

      }

 

      // Let's register the application to load on AutoCAD

      // startup (2) if specified or if it contains no

      // commands. Otherwise we will have it load on

      // command invocation (12)

 

      int flags = (!startup && globCmds.Count > 0 ? 12 : 2);

 

      // Now create our Registry keys

 

      return CreateDemandLoadingEntries(

        name, path, globCmds, locCmds, groups,

        flags, currentUser, force

      );

    }

 

    public static bool UnregisterForDemandLoading(Assembly assem)

    {

      // Get the name of the application to unregister

 

      string appName = assem.GetName().Name;

 

      // Unregister it for both HKCU and HKLM

 

      bool res = RemoveDemandLoadingEntries(appName, true);

      res &= RemoveDemandLoadingEntries(appName, false);

 

      // If one call failed, we also fail (could change this)

 

      return res;

    }

 

    // Helper functions

 

    private static bool CreateDemandLoadingEntries(

      string appName,

      string path,

      List<string> globCmds,

      List<string> locCmds,

      List<string> groups,

      int flags,

      bool currentUser,

      bool force

    )

    {

      string ackName = GetAutoCADKey();

      RegistryKey hive =

        (currentUser ? Registry.CurrentUser : Registry.LocalMachine);

 

      // We may need to create the Applications key, as some AutoCAD

      // verticals do not contain it under HKCU by default

 

      // CreateSubKey just opens existing keys for write, anyway

 

      RegistryKey appk =

        hive.CreateSubKey(ackName + "\\" + "Applications");

      using (appk)

      {

        // Already registered? Just return (unless forcing)

 

        if (!force)

        {

          string[] subKeys = appk.GetSubKeyNames();

          foreach (string subKey in subKeys)

          {

            if (subKey.Equals(appName))

            {

              return false;

            }

          }

        }

 

        // Create the our application's root key and its values

 

        RegistryKey rk = appk.CreateSubKey(appName);

        using (rk)

        {

          rk.SetValue(

            "DESCRIPTION", appName, RegistryValueKind.String

          );

          rk.SetValue("LOADCTRLS", flags, RegistryValueKind.DWord);

          rk.SetValue("LOADER", path, RegistryValueKind.String);

          rk.SetValue("MANAGED", 1, RegistryValueKind.DWord);

 

          // Create a subkey if there are any commands...

 

          if ((globCmds.Count == locCmds.Count) &&

              globCmds.Count > 0)

          {

            RegistryKey ck = rk.CreateSubKey("Commands");

            using (ck)

            {

              for (int i = 0; i < globCmds.Count; i++)

                ck.SetValue(

                  globCmds[i],

                  locCmds[i],

                  RegistryValueKind.String

                );

            }

          }

 

          // And the command groups, if there are any

 

          if (groups.Count > 0)

          {

            RegistryKey gk = rk.CreateSubKey("Groups");

            using (gk)

            {

              foreach (string grpName in groups)

                gk.SetValue(

                  grpName, grpName, RegistryValueKind.String

                );

            }

          }

        }

      }

      return true;

    }

 

    private static bool RemoveDemandLoadingEntries(

      string appName, bool currentUser

    )

    {

      try

      {

        string ackName = GetAutoCADKey();

 

        // Choose a Registry hive based on the function input

 

        RegistryKey hive =

          (currentUser ?

            Registry.CurrentUser :

            Registry.LocalMachine);

 

        // Open the applications key

 

        RegistryKey appk =

          hive.OpenSubKey(ackName + "\\" + "Applications", true);

        using (appk)

        {

          // Delete the key with the same name as this assembly

 

          appk.DeleteSubKeyTree(appName);

        }

      }

      catch

      {

        return false;

      }

      return true;

    }

 

    private static string GetAutoCADKey()

    {

      // Start by getting the CurrentUser location

 

      RegistryKey hive = Registry.CurrentUser;

 

      // Open the main AutoCAD key

 

      RegistryKey ack =

        hive.OpenSubKey(

          "Software\\Autodesk\\AutoCAD"

        );

      using (ack)

      {

        // Get the current major version and its key

 

        string ver = ack.GetValue("CurVer") as string;

        if (ver == null)

        {

          throw new System.Exception(

            "Could not find major CurVer."

          );

        }

        else

        {

          RegistryKey verk = ack.OpenSubKey(ver);

          using (verk)

          {

            // Get the vertical/language version and its key

 

            string lng = verk.GetValue("CurVer") as string;

            if (lng == null)

            {

              throw new System.Exception(

                "Could not find local/vertical CurVer."

              );

            }

            else

            {

              RegistryKey lngk = verk.OpenSubKey(lng);

              using (lngk)

              {

                // And finally return the path to the key,

                // without the hive prefix

 

                return lngk.Name.Substring(hive.Name.Length + 1);

              }

            }

          }

        }

      }

    }

  }

}

I won't go into greater detail regarding the specific changes, I'll just leave you with the updated source project and executable.

Update

I've updated the source and downloadable project to fix the issue reported in this comment.

11 responses to “An updated version of RegDL available”

  1. I had a problem with your RegDL application when using it with an installer as described earlier.
    The problem is when the .dll you want to register uses a type from another .dll.
    When RegDL is in ".\install" it can only find types in that directory. To fix it I changed some code as follow:


    // As each dependent assembly is resolved, we need to make
    // sure it is loaded via Reflection-Only, not a full Load
    AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve +=
    delegate(object sender, ResolveEventArgs rea)
    {
    string simpleName = new AssemblyName(rea.Name).Name;
    string dllFile= Path.Combine(Path.GetDirectoryName(asmName), simpleName + ".dll");
    if (File.Exists(dllFile))
    {
    return Assembly.ReflectionOnlyLoadFrom(dllFile);
    }
    return Assembly.ReflectionOnlyLoad(rea.Name);
    };

    Thanks for your articles they help me a lot!

  2. Interesting - thanks!

    Kean

  3. This is very useful, but there is one thing but - RegDL does not overwrite the value in the registry, if there already is.

  4. If that's the behaviour you desire, it should be trivial to implement (and the full source code is provided for you to extend as you see fit).

    Kean

  5. oh, sorry, my mistake - did not notice the option "force" ...

  6. Ah yes, I'd forgotten I'd already implemented it. :-S

    Kean

  7. I have the impression that, when writing the command names to the registry, there are some quotation marks too much added. As when entering that command in Autocad, the following output can be seen:

    Command: "MYCMD"
    Loading MyModule...
    Unknown command ""MYCMD"". Press F1 for help.
    Means loading of the Module works, but immediate execution of the command not.
    If looked in the registry, the quotation marks can also be seen, as when compared with another project where the loading works, there are none...

    1. What happens if you enter the command name without quotations marks at the command line?

      They shouldn't be there: did you find the problem running the code in this post?

      Kean

      1. Thanks for your quick reply... 😮
        This happens also if I type the command to the command line without quotation marks.
        The only change that I've made to your project was to use .net framework 4 to have it run for my Autocad 2014 .net dlls.

        Silke

        1. Kean Walmsley Avatar

          Very strange... I haven't seen any other complaints about this (and the post is quite old now). Could it be a copy & paste issue of the code into your project?

          Kean

          1. SilkeFörnzler Avatar
            SilkeFörnzler

            No as I haven't copied the code at all... just downloaded your project, converted it for 2014 and changed .net framework to 4. Then recompiled and used it...
            Silke

Leave a Reply to Silke Cancel reply

Your email address will not be published. Required fields are marked *