Geeks With Blogs
.NET Corner Jeans, .NET and Physics (eka The Quantum Boy)
Get the short introductory part 1 here.

After the initial inspection was finished, I was pretty much surprised by looking at the existing assembly unGACing code. The code expects full path of the concerned assembly, create a temporary appdomain, inject following class(Util) into the appdomain and get the full qualified name(strong name) for the assembly, then uninstall with the help of IRegister interface.

internal class Util : MarshalByRefObject
{
internal string FullAssemblyName(string name)
{
return Assembly.LoadFrom(name).FullName;
}


Crap to say at least. The first thing I did was to learn something about GACed assembly enumeration. If any COM enumeration interface exist, wrap it up in a .NET enumerator or create a custom GAC assembly enumerator from scratch. The second option needs explicit knowledge about GAC folder hierarchy under the hood of %WINDIR%\assembly folder. This would expose my implementation to break often as GAC changes internally. Not to mention 64 bit GAC handling, because in a typical 64 bit box different GACs can exist - GAC_32(32 bit CLR), GAC_64(64 bit CLR), GAC_MSIL(Any CPU) etc. So the first option wins. Fortunately enough there is a relevant COM enumeration interface.
[SuppressUnmanagedCodeSecurityAttribute()]
[ComImport, Guid("21B8916C-F28E-11D2-A473-00C04F8EF448"), InterfaceType(1)]
internal interface IAssemblyEnum
{
[PreserveSig]
int GetNextAssembly([Out] out IApplicationContext ppAppCtx, [Out] out IAssemblyName ppName, uint dwFlags);
[PreserveSig]
int Reset();
[PreserveSig]
int Clone([Out] out IAssemblyEnum ppEnum);
}

This demands the declarations of the following interfaces - IApplicationContext and IAssemblyName

[SuppressUnmanagedCodeSecurityAttribute()]
[ComImport, Guid("7C23FF90-33AF-11D3-95DA-00A024A85B51"), InterfaceType(1)]
internal interface IApplicationContext
{
void SetContextNameObject(IAssemblyName pName);
void GetContextNameObject([Out] out IAssemblyName ppName);
void Set([MarshalAs(UnmanagedType.LPWStr)] string szName, int pvValue, uint cbValue, uint dwFlags);
void Get([MarshalAs(UnmanagedType.LPWStr)] string szName, [Out] out int pvValue, ref uint pcbValue, uint dwFlags);
void GetDynamicDirectory([Out] out int wzDynamicDir, ref uint pdwSize);
}


[SuppressUnmanagedCodeSecurityAttribute()]
[ComImport, Guid("CD193BC0-B4BC-11D2-9833-00C04FC31D2E"), InterfaceType(1)]
internal interface IAssemblyName
{
[PreserveSig]
int SetProperty(uint PropertyId, IntPtr pvProperty, uint cbProperty);
[PreserveSig]
int GetProperty(uint PropertyId, IntPtr pvProperty, ref uint pcbProperty);
[PreserveSig]
int Finalize();
[PreserveSig]
int GetDisplayName(IntPtr szDisplayName, ref uint pccDisplayName, uint dwDisplayFlags);
[PreserveSig]
int BindToObject(object refIID, object pAsmBindSink, IApplicationContext pApplicationContext, [MarshalAs(UnmanagedType.LPWStr)] string szCodeBase, long llFlags, int pvReserved, uint cbReserved, [Out] out int ppv);
[PreserveSig]
int GetName(ref uint lpcwBuffer, [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pwzName);
[PreserveSig]
int GetVersion([Out] out uint pdwVersionHi, [Out] out uint pdwVersionLow);
[PreserveSig]
int IsEqual(IAssemblyName pName, uint dwCmpFlags);
[PreserveSig]
int Clone([Out] out IAssemblyName pName);


Now the time for .NET wrapper on top of the COM enumeration interface

internal abstract class AssemblyCacheEnumerator : IEnumerator
{
protected AssemblyCacheEnumerator(string assemblyName,int cache)
{
//Assembly name passed in may be partial, therefore enumerate matching assemblies //for uninstallation of each one. IAssemblyName fusionName = null;
int hr = 0;

//Create the COM enumeration object Fusion.CreateAssemblyEnum(out this.m_ae, null, fusionName, (uint)cache, 0);
}

bool IEnumerator.MoveNext()
{
IApplicationContext context1 = null;
return (0 == this.m_ae.GetNextAssembly(out context1, out this.m_anCurrent, 0));
}

void IEnumerator.Reset()
{
this.m_ae.Reset();
}

// Properties object IEnumerator.Current { get { return this.m_anCurrent;
}
}

// Fields private IAssemblyEnum m_ae;
private IAssemblyName m_anCurrent;

}

Now this is a generic assembly cache enumerator not a GAC enumerator. Assembly cache has three subzones - GAC, NGenCache and DownloadCache.

[Flags]
internal enum AssemblyCacheFlags
{
NGenCache = 0x1,
GAC = 0x2,
DownloadCache = 0x4
}

For GAC subzone following subclass is defined

internal class GacEnumerator : AssemblyCacheEnumerator
{
public GacEnumerator(string assemblyName) : base(assemblyName,(int)AssemblyCacheFlags.GAC)
{
}
}

It's time for the core Fusion interface -  IRegister

[SuppressUnmanagedCodeSecurityAttribute()]
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("e707dcde-d1cd-11d2-bab9-00c04f8eceae")]
internal interface IRegister
{
[PreserveSig()]
int UninstallAssembly(uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] string pszAssemblyName, IntPtr pvReserved, out uint pulDisposition);
[PreserveSig()]
int QueryAssemblyInfo(uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] string pszAssemblyName, IntPtr pAsmInfo);
[PreserveSig()]
int CreateAssemblyCacheItem(uint dwFlags, IntPtr pvReserved, out /*IRegisterItem*/IntPtr ppAsmItem, [MarshalAs(UnmanagedType.LPWStr)] String pszAssemblyName);
[PreserveSig()]
int CreateAssemblyScavenger(out object ppAsmScavenger);
[PreserveSig()] to be
int InstallAssembly(uint dwFlags, [MarshalAs(UnmanagedType.LPWStr)] string pszManifestFilePath, IntPtr pvReserved);
}

And here goes the core Fusion API

[DllImport("Fusion.dll", CharSet=CharSet.Auto)]
internal static extern int CreateAssemblyCache(out IRegister ppAsmCache, uint dwReserved);

The main unGAC function basically creates the assembly cache, create the enumerator, get the strong name for each IAssemblyName references and call UninstallAssembly function on IRegister interface. But there was a small problem. While "partial" assembly name was passed, it was not expanding to all its matches. There has to be something missing. MSDN docs were helpless. Then as a fallback I digged into SSCLI. I found following unmanaged(C++) code at http://dotnet.di.unipi.it/Content/sscli/docs/doxygen/tools/gac/gac_8cpp-source.html

//Name passed in may be partial, therefore enumerate matching assemblies
00357     //and uninstall each one. Uninstall API should be called with full name ref.
00358
00359     //Create AssemblyName for enum
00360     if ((hr = (*g_pfnCreateAssemblyNameObject)(&pEnumName, pszAssemblyName, CANOF_PARSE_DISPLAY_NAME, NULL)))
00361     {
00362         BSILENT_PRINTF0ARG("Failure removing assembly from cache: ");
00363         ReportError(hr);
00364         if (pCache) pCache->Release();
00365         return false;
00366     }
00367    
00368
00369     //
00370     // For zaps, null out the custom string
00371     if (bzapCache)
00372     {
00373             DWORD dwSize = 0;
00374             //Check if Custom string has been set
00375     hr = pEnumName->GetProperty(ASM_NAME_CUSTOM, NULL, &dwSize);
00376
00377             if (!dwSize)
00378             {
00379             //Custom String not set - set to NULL to unset property so lookup is partial
00380                     pEnumName->SetProperty(ASM_NAME_CUSTOM, NULL, 0);
00381             }
00382     }
00383
00384     hr = (*g_pfnCreateAssemblyEnum)(&pEnum,
00385                             NULL,
00386                             pEnumName,
00387                             bzapCache ? ASM_CACHE_ZAP : ASM_CACHE_GAC,
00388                             NULL);

Note the difference with the AssemblyCacheEnumerator constructor - An extra call to CreateAssemblyNameObject Fusion API which translates to following C# code

IAssemblyName fusionName = null;
int hr = 0;

if (assemblyName != null)
{
//Create AssemblyName for enumeration hr = Fusion.CreateAssemblyNameObject( out fusionName, assemblyName, CreateAssemblyNameObjectFlags.CANOF_PARSE_DISPLAY_NAME, 0); } //Create the COM enumeration object Fusion.CreateAssemblyEnum(out this.m_ae, null, fusionName, (uint)cache, 0);

The main uninstall function is given below

static internal int UnRegFile(string assembly)
{
IRegister ac = null;
uint n;
int hr = CreateAssemblyCache(out ac, 0);
if (hr != 0)
return hr;
else { IAssemblyName name = null;
IEnumerator enumerator = null;

//Create the GAC enumerator enumerator = new GacEnumerator(assembly);

string sDisplayName = null;
while (enumerator.MoveNext())
{
//Get the assemblyname from the set of GACed assemblies that matched //with the given partial name.If full name is provided this set will //contains exactly one element name = (IAssemblyName) enumerator.Current; //Get the displayname of the assembly sDisplayName = Fusion.GetDisplayName(name,AssemblyNameDisplayFlags.ALL); #if VS8
//Here we will count only .NET 2.0 assemblies(which has the processorArchitecture //attribute as part of assembly strongname. if (sDisplayName.Contains(processorArchitecturex86) ||
sDisplayName.Contains(processorArchitecturemsil))
{
#endif
//The following API always expects a full name instead of a partial name hr = ac.UninstallAssembly(0, sDisplayName.ToString(), (IntPtr)0, out n); if (hr != 0)
break;
#if VS8
}
#endif

//Release COM object Marshal.ReleaseComObject(name); } return hr;
}
}


All the DllImport declarations for Fusion APIs are pushed into an internal class called Fusion which also houses the implementation of the GetDisplayName function

internal class Fusion
{
[DllImport("Fusion.dll", CharSet=CharSet.Auto)]
public static extern int CreateAssemblyEnum(out IAssemblyEnum ppEnum, IApplicationContext pAppCtx, IAssemblyName pName, uint dwFlags, int pvReserved);

[DllImportAttribute("Fusion.dll", CharSet = CharSet.Unicode)]
public static extern int CreateAssemblyNameObject(
out IAssemblyName ppEnum,
string szAssemblyName, // A string representation of the assembly name or of a full assembly reference that //is determined by dwFlags. The string representation can be null. CreateAssemblyNameObjectFlags dwFlags, int pvReserved); // Must be null. internal static string GetDisplayName(IAssemblyName aName, AssemblyNameDisplayFlags displayFlags)
{
uint uiLen = 0;
string sDisplayName = null;
aName.GetDisplayName(IntPtr.Zero, ref uiLen, (uint)displayFlags);
if (uiLen > 0)
{
IntPtr ptr1 = IntPtr.Zero;
byte[] buffer1 = new byte[(uiLen + 1) * 2];
unsafe
{
fixed (byte* numRef1 = buffer1)
{
ptr1 = new IntPtr((void*) numRef1);
aName.GetDisplayName(ptr1, ref uiLen, (uint)displayFlags);
sDisplayName = Marshal.PtrToStringUni(ptr1);
}
}
}
return sDisplayName;
}
}

Congrats! You've successfully recreated the gacutil utility.

Re: Although this utility gives you somewhat similar functionality of gacutil, in a typical production deployment MSI should be used. Aaron Stebner has pointed that out here -
http://blogs.msdn.com/astebner/archive/2006/11/04/why-to-not-use-gacutil-exe-in-an-application-setup.aspx

-Thanks
Deb.

Posted on Friday, November 7, 2008 10:04 AM .NET Core | Back to top


Comments on this post: Recreate gacutil - Part 2

No comments posted yet.
Your comment:
 (will show your gravatar)


Copyright © dbose | Powered by: GeeksWithBlogs.net