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.