Apps Expire from Local Cache, part 3

Okay, things get stranger.

In the case where the package isn't found on the local cache... it's actually there, it's identical to the original package, permissions are set correctly, but yet the Installer engine doesn't recognize it... still troubleshooting.

RootDSE without WINS

We're removing WINS from our environment.  Don't ask me why, I'm not a big fan of the idea... but they don't pay me to make decisions, they pay me to make things work.

Things work fine without WINS, once you've joined the domain, DNS takes it all over and everything works.

However, we have a utility that does most of those tasks needed to join a machine to the domain -- creates an INI file used to create the machine account, renames the machine to meet our standards, and does the join (among other things).  The person joining the domain specifies their username, password, and their user domain.

At pilot sites where WINS is no longer available, the app started failing.  Research found it was the call to LDAP to try to find the RootDSE.  To reproduce, set your primary and secondary WINS addys to be an invalid IP.  Adding company.com as a DNS suffix makes it work, but apparently the networking folks don't want to add a suffix to the DHCP configuration.

If we go with something along the lines of

DirectoryEntry rootEntry = new DirectoryEntry(LDAP://domain/RootDSE);

it fails with "The server is not operational"

However, specifying the FQDN works:

DirectoryEntry rootEntry = new DirectoryEntry(LDAP://domain.company.com/RootDSE);

So, hopefully this will help someone, although if you're more of a networking guy than me it was probably obvious all along.

Apps Expire from Local Cache, part 2

Even though I'm no longer involved with group policy, I'm still the go-to guy for issues.

Got a call this morning from a helpdesk guy who was working an issue with Add New Programs not populating.  He'd dug through RSOP and found that two apps had failed to remove. (More specifically, RSOP showed the yellow bang on User Configuration/Software Settings/Software installation, with these two listed as "Removed Applications").  The details for these two apps specified "The removal of xxx from policy yyy failed.  The error was : The installation source for this product is not available.  Verify that the source exists and that you can access it"

A little digging found that both of these apps had been removed from policy with the "Immediately Uninstall" option months ago, and somewhere along the way the app folder was removed from the local servers.  One of the apps had the folder contents available; simply replacing it on the local server was sufficient to allow the uninstall to complete.

For the other app, the source no longer exists.  Most users were able to uninstall with the local cached copy, but this user no longer had that MSI.  Helpdesk will be using MSIZAP to get rid of the registry entries for this app so the user can install the newer revision of this app.

Not sure how long the user has been having trouble.  The lesson to be learned here is to not be in such a hurry to remove the source from the local servers; users may need it for a while after if you've removed it from policy with "Immediately Uninstall", or if you're going to ever push out an app as an upgrade to this one.

Change of duties

The Powers That Be have decreed that the work I've been doing with group policy will be transitioned to another group, so I won't be involved with that anymore.

Since I won't have any things I've found with GP anymore, I guess future posts will involve my other two roles: application packaging and being the junior coder... along with those things I learn in my endeavor to become a not-so-junior programmer -- I've got lots to learn in C#, plus a whole lot of C++ to learn (both native and managed).  Should be fun.

Additionally, I'm a full time college student as well.  There may be some interesting things I find there (or if future semesters are anything like the last one, postings may be sporadic while I scramble to keep on top of my classes)

New book

Okay, it's not new; but it's new to me.  It's the Microsoft Windows Group Policy Guide.  I'm not sure why I didn't pick this up sooner; it's got some useful knowledge for group policy in it (go figure).

What GPOs applied to this machine?

I was asked earlier today to come up with a list of what GPOs applied to a given box.  Simple enough given the code from one of the previous posts to ask AD what GPOs apply to the machine's OU, but I'm not convinced there's more to it than that.
 
Senior coder had started going through GetGPOList(), FreeGPOList(), and GetAppliedGPOList() but they didn't really play well in C# from an interop standpoint.  I'll likely give them a try at a later time.
 
Instead, today I went looking through the registry and came across HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\GPLink-List -- hey look, Windows keeps track of what GPOs apply, and unlike the "Machine OU" option I mentioned above, it also deals with inheritance.
 
The task seems pretty clear: Go through the subkeys of the GPLink-List key, and translate the DsPath entry from a GUID to a name. 
 
        private List<string> GetListOfGPOs()
        {
            List<string> GPOList = new List<string>();
            string Keyname = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\GPLink-List";
            RegistryKey rk = Registry.LocalMachine.OpenSubKey(Keyname);
            string[] keys = rk.GetSubKeyNames();
            foreach(string GPOKey in keys)
            {
                RegistryKey theKey = Registry.LocalMachine.OpenSubKey(Keyname + "\\" + GPOKey);
                string Value = theKey.GetValue("DSPath", "").ToString();
                if (Value.Contains("cn="))
                    GPOList.Add(TranslateFromGUIDToName(Value));
                else
                    GPOList.Add(Value); // Special case: "LocalGPO"
            }
            return GPOList;
        }

 

 and of course, we need a function to translate the GUID (actually the entire path) to the policy name.  You might want additional info as well, all we were looking for was the name.  Adjust the Properties you're looking for to suit:

           private string TranslateFromGUIDToName(string path)

        {
            using (DirectorySearcher mySearcher = new DirectorySearcher(new DirectoryEntry("LDAP://" + path, null, null, AuthenticationTypes.Secure)))
            {
                mySearcher.ClientTimeout = new TimeSpan(0, 0, 10);
                mySearcher.SearchScope = SearchScope.Subtree;
 
                try
                {
                    SearchResult result = mySearcher.FindOne();
                    if (result != null)
                    {
                        if (result.Properties["displayName"][0] != null)
                        {
                            mySearcher.Dispose();
                            return result.Properties["displayName"][0].ToString();
                        }
                    }
                }
                catch (Exception ex)
                {
                    return ex.Message;
                }
            }
            return "Not found.";
        }
So there you have it.  Not all that useful -- just giving the names of the policies that applied, but it's a start.

Error when publishing apps in GPO:

We got an error yesterday that I'd not seen before, when publishing two separate packages.  Both of them were created by Visual Studio 2008 instead of our typical Wise/InstallShield packages.

The error was "No package in the software installation data in the Active Directory meets this criteria.". 

A web search found nothing useful on this error; I'd started digging into it to maybe be the first to provide something on it -- but during troubleshooting both packages were published without error.  MSI logging wasn't on, so I've got insufficient details to give any theory.  So.. if you get this error, I guess try again later. 

Anybody got a better answer?  I'm still curious about the cause of the error.

Apps Expire from local MSI cache

I've had this post rattling around in my head for a few months now, but I've never had enough hard data to really flesh it out as well as I'd liked.

I still don't, but I need to get this down on paper.

Apps deployed via Group Policy expire from the local cache.  This can be a problem if the app is removed from the servers, or if you make changes to an existing app that would make its MSI not match the installation (Product/Package code changes, etc).

I don't have the details as to why - whether it's the oldest that gets purged, or least commonly used, or largest; whenever the problem has been seen here, it gets fixed first and the why gets worried about later.  I'd hoped that the next batch would last long enough for me to dig out the details but no such luck.

Want to reproduce the problem?  Simple: 

1.  Publish an app via Group Policy.  Let thousands of people install it.

2.  Let it sit for a while (like I said: I don't have the details as to what causes it to expire)

3.  Change the product code in the MSI on the server.

4.  Try to uninstall the package on the client.  MSI not found, tries to recache from the network, not found there either.

Ran this past a coworker who is an MSI guru; he ran it past his folks and nobody had heard of MSIs not staying in the local cache.  It is entirely possible that it's something specific to our implementation (a policy setting?).  It seems to manifest itself as the same old "Installation Source cannot be found" message when users attempt to repair, uninstall, or upgrade an app.

Have you seen this?  Do you know anything more about it?

Cleanup

Back to the basic apps I started with.

There were a couple of locations where the output was "System.Byte[]" or "System.Object[]". 

System.Object[] appears to happen in "msifilelist", and only on those apps that have a transform.

Given that you're loading everything into a DataRow, code like this will separate out the transform if one exists:

foreach (object a in myDirectoryEntry.Properties["msifilelist"])

{

if (a.ToString().ToLower().EndsWith(".mst"))

dr["Transforms"] = a.ToString();

else

dr["msifilelist"] += a.ToString();

}

The System.Byte[] is a bit more complex.

In a similar way,

foreach (object a in myDirectoryEntry.Properties["productcode"])

 

works to get the bytes out of the ProductCode property, but the object contains an array.  You can't access the contents of an array from within an object -- a[1] isn't valid, and none of the casts I tried worked.

 

Found some code online to convert an object to an array of bytes.  It works, to an extent, but there's quite a bit of extra stuff tacked on to both the beginning and the end of the array.  I don't know why - it's got to come from something in that function, but I can't tell where.

 

Simple enough to loop through and clean it up:

StringBuilder sb = StringBuilder();
byte[] code = ObjectToByteArray(a);
int i = 0;
foreach (byte b in code)
{
if (i++ >= 27)
{
sb.Append(b.ToString("X2"));
}
}
string outString = ToString();
However, the GUID as returned from AD doesn't match what it should.  Not only is this just an array of bytes, but the dashes are missing and the bytes in the first three sections are in a jumbled order.
One would have thought that
Guid g = new Guid(outstring);
outString = g.ToString("B");
would have taken care of that -- and it does add in the dashes, but the byte order is still off.. and by off, I mean like this:
If the proper GUID is {12345678-ABCD-1234-ABCD-123456789AB}, the returned GUID is {78563412-CDAB-3412-ABCD-123456789AB}.  I know this is a side effect of how Windows handles GUIDs internally, and there's probably an easy way to fix it, but I couldn't find it.  So, I did my own:
            StringBuilder newString = new StringBuilder();
            newString.Append("{");
            newString.Append(outString.Substring(6, 2));
            newString.Append(outString.Substring(4, 2));
            newString.Append(outString.Substring(2, 2));
            newString.Append(outString.Substring(0, 2));
            newString.Append("-");
 
            newString.Append(outString.Substring(10, 2));
            newString.Append(outString.Substring(8, 2));
            newString.Append("-");
 
            newString.Append(outString.Substring(14, 2));
            newString.Append(outString.Substring(12, 2));
            newString.Append("-");
 
            newString.Append(outString.Substring(16, 4));
            newString.Append("-");
 
            newString.Append(outString.Substring(20, 12));
            newString.Append("}");
 
            return newString.ToString().ToUpper();

So, there you have it.  Nothing groundbreaking, but if you're just trying to get the product or upgrade codes from AD, here's the missing piece from the code I already put up.  I would have expected the Package Code to be in AD as well, but it's not.

Time granularity in Robocopy

We came across an issue where Robocopy was thinking that many files on a given server had been modified from the version that was on a different server.
Not a big deal, except "changed files" = "files that need to be copied out", and there were a lot of files that would have been copied over slow links to distant servers.
The wierd thing - some folks were seeing the problem with Robocopy, and some were not.  Finally narrowed it down.. the people who weren't seeing the problem were on an older version of Robocopy (confirmed 9-4-07: v 1.95) and the folks who were were on XP010.  However, that doesn't explain the root cause.
If you looked at the two files in Explorer, the Modified dates were identical.  The files themselves were identical.  However, Robocopy said one was Newer.
Wrote a stupid little app that gave the Modified time down to the millisecond.. still identical.
However, when I got down to the Ticks level (100 nanosecond), the files had different timestamps.. a mirroring utility that was in use on some servers only replicated the time down to the millisecond.  The old Robocopy only looked as far as Milliseconds, where the new one (and our primary directory replication tool) looked all the way at the Ticks level.
The fix?  Wrote another stupid little app that compared two files - if the date, hour, minute, second, and millisecond were the same, but one of them had zeroes in the last four characters of the Ticks value (ticks % 10000), set the time of the zero to be the time of the not-zero.  Beats copying all those files out.
Code?  Here you go:
 
long filetick = File.GetLastWriteTime(file).Ticks;
long badfiletick = File.GetLastWriteTime(badfile).Ticks;
DateTime fileDT = File.GetLastWriteTime(file);
DateTime badDT = File.GetLastWriteTime(badfile);
bool filetrunc = false;
bool badtrunc = false;
filetrunc = IsTruncated(filetick);
badtrunc = IsTruncated(badfiletick);
   if ((fileDT.Date == badDT.Date) && (fileDT.Hour == badDT.Hour) && (fileDT.Minute == badDT.Minute) && (fileDT.Second == badDT.Second) && (fileDT.Millisecond == badDT.Millisecond))

      if (filetrunc || badtrunc)
           if (!(filetrunc && badtrunc))

                {

                      {

                          if (filetrunc)

                              {

                               File.SetLastWriteTime(file, File.GetLastWriteTime(badfile));

                               }

                          else if (badtrunc)

                               {

                               File.SetLastWriteTime(badfile, File.GetLastWriteTime(file));

                               }

                       }

                 }
and



        public bool IsTruncated(long ticks)


        {

            return ((ticks % 10000) == 0);

        }

As always, if there's a better way to do this, please let me know - I'm still a junior coder.

App Distribution across domains

This one is more for my benefit than for yours... the base question for this one is a question that I've been asked more than once at work, and even more than once by the same person.  I figure if I write it out, perhaps I'll be able to explain it better.

Okay - first, the players.

Two domains.  Let's call them DomA and DomB.

Two users.  We can call them UserA and UserB, and for the sake of discussion, we'll put them in their own domains: DomA\UserA and DomB\UserB.

Two app policies (published apps) - one per domain.  Let's call them AppPolicyDomA and AppPolicyDomB to make it easy.

A couple of apps per policy.  Let's go with one that's locked down with a domain-specific group (DomA\MyAppGroup and DomB\MyOtherAppGroup) and one that's open to Authenticated Users.  I don't think I'll need app names.

Okay, given all these players, the base question is:

"If a user from DomB logs onto a machine in DomA, what apps will he (or she) be able to install?"

One opinion that was expressed was that because Loopback Merge was turned on, it'd redirect processing back to the user's own domain.  That's not actually right though, and here's why.

Any given machine - be it in DomA or in DomB, will get only those policies that are linked to that specific machine's OU... and that's going to be domain-specific.  So, the contents of the user's Add/Remove programs will be those apps in those policies that are linked to that machine's OU and the user has rights to -- loopback merge or not, there's nothing that would tell this machine in DomA to go look at DomB's AppPolicyDomB to pull down the list. 

So.. what would DomB\UserB be able to install if he logged onto a machine in DomA?

1) Any app open to "Authenticated Users".  This could be a problem if the domains have different licensing schemes (DomA has a site license for the exact number of DomA users, etc).

2) Any app where DomB's user account is a member of the DomA group.  In our environment, that's pretty much none of them. 

3) All apps assigned to the machine will already be installed. 

Now, you could conceivably use WMI filtering to prevent the Auth Users apps from being available. Filtering was ruled out at a level well above my pay grade so I've done no digging into this one.

Okay, this may not have helped so much, but at least I can point folks at this next time they ask.

Deleting a 64 bit registry entry from a 32 bit app in C#.

Okay, this may be old hat to some of you, but it took me some time to figure it out and I didn't find much useful when searching.

Came across a need to delete a 64 bit registry value from a 32 bit app.  The standard registry delete functions would end up deleting it from the Wow6432Node, which was what was not the desired outcome.

The code to make it happen is pretty simple, but I've got no error checking in the sample code here:

First, the P/Invoke stuff:

 [DllImport("advapi32.dll")]

static extern int RegOpenKeyEx(

      RegistryHive hKey,

      [MarshalAs(UnmanagedType.VBByRefStr)] ref string subKey,

      int options,

      int sam,

      out UIntPtr phkResult );

 

 [DllImport("advapi32.dll")]

static extern int RegDeleteValue(

      UIntPtr hKey,

      [MarshalAs(UnmanagedType.VBByRefStr)] ref string lpValueName

      );

And then, the meat of the program:

      UIntPtr KeyHandle = UIntPtr.Zero;

      string key = @"Software\Microsoft\Windows NT\CurrentVersion\TestKey";

      string value = "DeleteThisValue";

      RegOpenKeyEx(RegistryHive.LocalMachine,ref key,0,0x000f013f,out KeyHandle);

      RegDeleteValue(KeyHandle,ref value);

The magic numbers 0 and 0x000f103f shouldn't be hardcoded; the 0 is "Non-volatile registry" and the hex number is both "KEY_WOW64_64KEY" (0x100) and "KEY_ALL_ACCESS (0xf003f)".

Converting a policy name to a GUID

Ok, part of all this we've already talked about requires that you have the policy's GUID.  But how do you get it?

Simple enough:

        private string Query(string Criteria, string Attribute)

        {

           DirectorySearcher mySearcher = new DirectorySearcher(new DirectoryEntry("LDAP://DC=DOMAIN,DC=MYCOMPANY,DC=com",    null,    null,    AuthenticationTypes.Secure    ));

            

            mySearcher.ClientTimeout = new TimeSpan(0, 0, 10);

            mySearcher.Filter = Criteria;

            mySearcher.SearchScope = SearchScope.Subtree;

 

            try

            {

                SearchResult result = mySearcher.FindOne();

 

                //if exception was not thrown, means it connected successfuly

                if (result != null)

                {

                    if(result.Properties[Attribute][0] != null)

                    {

                        mySearcher.Dispose();

                        return result.Properties[Attribute][0].ToString();

                    }

                }

            }

            catch (Exception ex)

            {

                return ex.Message;

            }

 

            mySearcher.Dispose();

            return "Not found.";          

        }

 

with the provision that the "Criteria" and "Attribute" arguments are, respectively,

 

"(&(objectCategory=groupPolicyContainer)(name=" + GUID + "))"  and "displayName" to convert from GUID to policy name, and

"(&(objectCategory=groupPolicyContainer)(displayName=" + Policy Name + "))","name" to go from policy name to GUID.

Package flags, part 2

I found my more detailed notes on the package flags.  A couple of corrections:

 

The flag “524288” specifically tells whether an app is published or assigned – it’s set for assigned, unset for published.

 

8 is typically set for published apps and cleared for assigned.

 

I promised code.

 

private void SearchAD(string target, string policy, string policyname)

        {

            DirectoryEntry entry = null;

            try

            {

                entry = new DirectoryEntry(policy);

            }

            catch (COMException Ex)

            {

                toolStripStatusLabel1.Text = "Couldn't connect to the specified Active Directory Path - Error = " + Ex.Message + Ex.InnerException;

                return;

            }

 

            DirectorySearcher mySearcher = new DirectorySearcher(entry);

            TimeSpan waitTime;

 

            try

            {

                waitTime = new TimeSpan(0, 0, 60); //hh--mm-ss

                mySearcher.ClientTimeout = waitTime; //wait this much time to display results

            }

            catch (Exception Ex)

            {

                toolStripStatusLabel1.Text = "Error = " + Ex.Message + Ex.InnerException;

                return;

            }

 

            try

            {

                SearchResult result = mySearcher.FindOne();

 

 

                //if exception was not thrown, means it connected successfuly

                if (result != null)

                {

                    // Get the 'DirectoryEntry' that corresponds to 'mySearchResult'.

                    DirectoryEntry myDirectoryEntry = result.GetDirectoryEntry();

 

 

                    // Get the properties of the 'mySearchResult'.

                    ResultPropertyCollection myResultPropColl;

                    myResultPropColl = result.Properties;

                    if (myDirectoryEntry.Properties["msiscriptname"].Value != null)

                    {

                        int flags = Int32.Parse(myDirectoryEntry.Properties["packageflags"].Value.ToString());

                       

                        if ((myDirectoryEntry.Properties["msiscriptname"].Value.ToString() != "R") || (cbRemoved.Checked))

                        {

                            if ((rbDisplayName.Checked == true) && (myDirectoryEntry.Properties["displayname"].Value.ToString().ToUpper().Contains(target)))

                            {

                                textBox1.Text += "\nInitial Flags: " + flags + "\n";

                                if (checkBox1.Checked)

                                    textBox1.Text += "Path: " + myDirectoryEntry.Path + "\n";

                                textBox1.Text += "App name: " + myDirectoryEntry.Properties["displayname"].Value + "\n";

                                textBox1.Text += "\tFound in policy " + policyname + "\n";

                                if (myDirectoryEntry.Properties["gPCFileSysPath"].Value != null)

                                {

                                    textBox1.Text += "\tPolicy Path: " + myDirectoryEntry.Properties["gPCFileSysPath"].Value + "\n";

                                }

                                if (myDirectoryEntry.Properties["msifilelist"].Value != null)

                                {

                                    textBox1.Text += "\tMSI Path: " + myDirectoryEntry.Properties["msifilelist"].Value + "\n";

                                }

                                if (myDirectoryEntry.Properties["whenchanged"].Value != null)

                                {

                                    textBox1.Text += "\tWhen changed: " + myDirectoryEntry.Properties["whenchanged"].Value + "\n";

                                }

                                if (myDirectoryEntry.Properties["msiscriptname"].Value.ToString() == "A")

                                    textBox1.Text += "Currently assigned.\n";

                                else if (myDirectoryEntry.Properties["msiscriptname"].Value.ToString() == "P")

                                    textBox1.Text += "Currently published.\n";

                                else if (myDirectoryEntry.Properties["msiscriptname"].Value.ToString() == "R")

                                {

                                    if ((flags & 128) != 0)

                                        textBox1.Text += "Removed from policy with allow users\n";

                                    else

                                        textBox1.Text += "Removed from policy with Immediately Remove!\n";

                                }

 

                                textBox1.Text += "Flags:";

(snipped a bunch of checking flags)

                                 textBox1.Text += "\n\n";

                            }

                        }

                    }

 

                    foreach (DirectoryEntry child in myDirectoryEntry.Children)

                    {

                        SearchAD(target, child.Path, policyname);

                    }

                }

            }

            catch

            {

                return;

            }

        }

 

It’s relatively self-explanatory; target is a string that contains part of the app’s displayname; policyname is just a friendly description of the policy name – such as “My-App-Deployment-Policy” or whatever.  Policy is the LDAP string for the policy – something like "LDAP://CN=Packages,CN=Class Store,CN=Machine,CN={BDA90329-B1C6-4E09-A60B-2BA6B19C54D6},CN=Policies,CN=System,DC=MY,DC=COMPANY,DC=com" or similar.

 

Oh, and a teaser for the next post:  Yes, you can un-remove and un-redeploy apps in AD.

 

(oh, and pasting everything into Word before copying/pasting here seems to make it all pretty)

My, how time flies.

Sorry about the delay.

Last time I dug into the SYSVOL portion of app distribution via Group Policy.  This time, the Active Directory side.

I'm sure you already know that the net result of GP distribution is that if the user is in the appropriate group, the app appears in Add/Remove programs.  The file on the Sysvol is important -- we've seen cases where for some reason the .AAS file disappeared; when it did, the app stopped appearing in Add/Remove Programs.

The other half is, of course, in Active Directory.  If you've done any playing with AD, you know that all the access to the contents is done via LDAP queries. 

Lots of data about a given app is stored in AD.  Here's a sample of the data stored about one application:

packageflags =     -1610610632
distinguishedname =     CN=42d7f80d-a031-49cf-8f39-c1f8cb9ed71b,CN=Packages,CN=Class Store,CN=User,CN={2137E375-D8FC-4CBB-9245-4219DBBAC43F},CN=Policies,CN=System,DC=MY,DC=COMPANY,DC=com
upgradeproductcode =     System.Byte[]
versionnumberlo =     0
whencreated =     12/21/2006 11:45:54 AM
packagetype =     5
localeid =     1033
packagename =     !dgfTest
revision =     0
name =     42d7f80d-a031-49cf-8f39-c1f8cb9ed71b
usnchanged =     3733596
lastupdatesequence =     20061221114659
objectcategory =     CN=Package-Registration,CN=Schema,CN=Configuration,DC=COMPANY,DC=com
showinadvancedviewonly =     True
msiscriptpath =     \\MY.COMPANY.COM\sysvol\MY.COMPANY.COM\Policies\{2537E375-D8FC-4CBB-9245-4219DBBAC43F}\User\Applications\{0A75EB3E-6003-46A3-A8CB-B102C9A19BDC}.aas
instancetype =     4
comclassid =     00000000-0000-0000-0000-000000000000:0
cn =     42d7f80d-a031-49cf-8f39-c1f8cb9ed71b
installuilevel =     5
msiscriptname =     P
objectclass =     top
    packageRegistration
usncreated =     3701378
objectguid =     System.Byte[]
displayname =     !TestApp
productcode =     System.Byte[]
msifilelist =     0:\\server\share\TestApp\TestApp.msi
whenchanged =     12/21/2006 4:20:04 PM
machinearchitecture =     1282
versionnumberhi =     1
adspath =     LDAP://CN=42d7f80d-a031-49cf-8f39-c1f8cb9ed71b,CN=Packages,CN=Class Store,CN=User,CN={2137E375-D8FC-4CBB-9245-4219DBBAC43F},CN=Policies,CN=System,DC=MY,DC=COMPANY,DC=com

A lot of this is self-explanatory:

WhenCreated is just that -- when the entry was created.

PackageName is the displayed name in Add/Remove.

MSIScriptPath is the path to the AAS.

WhenChanged is when the entry was touched - to do a Redeploy, change the displayed name, or similar.

MSIScriptName is not what it looks like - if it's P, the package is Published.  If it's A, the package is Assigned, and if it's R, the package has been Removed.  The details as to how it was removed ("Immediately uninstall" or "Allow users to continue") are stored in the PackageFlags, detailed later.

The Deployment Count is stored in "Revision" -- if you redeploy an app, this is incremented and the update times are updated.. that's all a Redeploy does.

The ProductCode and UpgradeCode should match the contents of the AAS and the MSI itself.  We haven't had a case where they differed, or at least not one where we noticed; so I can't specify what happens if they don't.

I've got another app that I published with the "Include COM Information" checked.. there's no difference in the AAS files, they always include the COM information, but there was a list of all the objects in AD when that was checked:

comprogid =     crystal.crystalreport (and a bunch of other stuff under comprogid)
comclassid =     0002560a-0000-0000-c000-000000000046:       1   (and a bunch more GUIDs). 

Our standard is to not include the COM information as to avoid bloating AD -- as you've can tell, removing a record doesn't clean up the used space.

The package flags were interesting - as the name suggests, they're bitflags.

I couldn't tell what values 1, 2, 4, 16, 32768, and 262144 meant, but I didn't find anything that had 16 set.

32 is "Display in Add/Remove Programs" (set for true)

64 is "Auto-Install by File Extension Activation"

128 is Removed with "Allow Users to Continue" option

256 is Removed with "Immediately Uninstall" option.  Go figure, 128 and 256 shouldn't both be set.  I don't know what happens if they are.

If PackageType is R, bits 128 and 256 specify what happens with existing installations.  I didn't see any examples where it was wrong.

512 shows that there is an Upgrade.

1024 is set when the app is Assigned.

2048 is "Uninstall when falls out of the scope of management" if it's false.

4096 should be the opposite of 2048.

If an app is a User Assigned app (automatic install per-user at logon), 8192 should be set.

16384 seemed to have two meanings: either a Per-Machine assignment (Computer Configuration) or a published app that was a Required Upgrade.

65536 selects whether an app is available to IA64 users - false for yes.

131072 is "Ignore Language"

524288 is UI Level "Basic".

So, if you've got a normal published app, the AD entries would look similar to this:

packageflags =     -1610610568
distinguishedname =     CN=6cbb5c46-d523-42a6-8a1c-8765c168721a,CN=Packages,CN=Class Store,CN=User,CN={2137E375-D8FC-4CBB-9245-4219DBBAC43F},CN=Policies,CN=System,DC=MY,DC=COMPANY,DC=com
upgradeproductcode =     System.Byte[]
versionnumberlo =     0
whencreated =     12/21/2006 11:47:09 AM
packagetype =     5
localeid =     1033
packagename =     !Test2
revision =     0
name =     6cbb5c46-d523-42a6-8a1c-8765c168721a
lastupdatesequence =     20061221114725
objectcategory =     CN=Package-Registration,CN=Schema,CN=Configuration,DC=COMPANY,DC=com
showinadvancedviewonly =     True
msiscriptpath =     file://MY.COMPANY.COM/sysvol/MY.COMPANY.COM/Policies/%7B2137E375-D8FC-4CBB-9245-4219DBBAC43F%7D/User/Applications/%7B99FF6442-0179-4961-B5D6-F5C56AFC3FE3%7D.aas
instancetype =     4
usnchanged =     3733588
installuilevel =     5
msiscriptname =     P
objectclass =     top
    packageRegistration
usncreated =     3701383
objectguid =     System.Byte[]
displayname =     !Test2
productcode =     System.Byte[]
msifilelist =     0:\\server\share\test2\test2.msi
whenchanged =     12/21/2006 4:20:04 PM
cn =     6cbb5c46-d523-42a6-8a1c-8765c168721a
machinearchitecture =     1282
versionnumberhi =     1
adspath =     ldap://CN=6cbb5c46-d523-42a6-8a1c-8765c168721a,CN=Packages,CN=Class/ Store,CN=User,CN={2137E375-D8FC-4CBB-9245-4219DBBAC43F},CN=Policies,CN=System,DC=MY,DC=COMPANY,DC=com

and then you go ahead and Remove with the "Immediately Uninstall" option, these are the items that would change:

packageflags =     -1610612464
lastupdatesequence =     20061221195037
usnchanged =     3758829
msiscriptname =     R
whenchanged =     12/21/2006 8:00:41 PM

Go figure the three changed counters would be updated.  MSIScriptName predictably changes to an R, and the package flags have a difference in 1024, 512, 256, 64, 32, and 8.

As mentioned, I don't know what 8 does.  Unsetting 32 hides the app in Add/Remove.  Unsetting 64 turns of the Auto-Install by Extension.  Turning off 256 is the "Immediately Uninstall" that we specified.  512 turns off any upgrades, and 1024 specifically unassigns the app.  My theory is that undoing these changes would stop an unintentional removal or redeploy but I haven't had a chance to do any testing on that (yet; but it's on my list).  No code today; I've got code that extracts all this stuff from AD but it's at work, and I'm not.  I'll try to get it posted this week.