Microsoft KB Archive/872800

From BetaArchive Wiki

Article ID: 872800

Article Last Modified on 5/18/2007



APPLIES TO

  • Microsoft .NET Framework 1.1
  • Microsoft .NET Framework 1.1 Service Pack 1
  • Microsoft Visual Studio .NET 2003 Enterprise Architect
  • Microsoft Visual Studio .NET 2003 Enterprise Developer
  • Microsoft Visual Studio .NET 2003 Professional Edition
  • Microsoft Visual Studio .NET 2003 Academic Edition





SYMPTOMS

An application that calls a Web service may take longer than expected to make the first call when compared to subsequent calls. This delay occurs every time that you run the application.

Note This hotfix cannot be applied on a system that is running the Microsoft .NET Framework 1.1 Service Pack 1. If you installed the .NET Framework 1.1 Service Pack 1 and are experiencing the symptom that is described in this article, request the hotfix rollup package that is described in Microsoft Knowledge Base article 890673 to obtain a resolution. For more information, click the following article number to view the article in the Microsoft Knowledge Base:

890673 Availability of the .NET Framework 1.1 Post-Service Pack 1 XML Web services and XML Messaging hotfix rollup package 8


CAUSE

The delay occurs when you run a client application for the first time because the Web service proxy uses a serialization assembly, and the serialization assembly is compiled dynamically at runtime. The serialization assembly then serializes and deserializes the information according to the client request and the server results that are returned for the request. The additional time on the first call occurs because of the compilation of the serialization assembly.

RESOLUTION

This hotfix must also be installed on the computers that run your applications that use pregenerated assemblies. To resolve this problem, follow these steps:

  1. Obtain and then install the hotfix. To do this, follow these steps:
    1. Obtain the hotfix from the following hotfix package:

      890673 Availability of the .NET Framework 1.1 Post-Service Pack 1 XML Web services and XML Messaging hotfix rollup package 8

    2. Extract the hotfix to a folder.
    3. Double-click the hotfix to install the hotfix on your computer.
  2. Pregenerate the serializer assembly. The following is a minimal, sample console application that will generate the serializer assemblies for a proxy assembly. For a more sophisticated version of this application, see the "More Information" section. To create the simple serializer pregenerator, follow these steps:
    1. Create a new Microsoft Visual C# .NET console application that is named PreGen.
    2. Rename the Class1.cs file to Pregen.cs.
    3. Replace all the existing code in the PreGen.cs file with the following code.

      namespace PreGenNS {
          using System;
          using System.Collections;
          using System.IO;
          using System.Reflection;
          using System.Xml.Serialization;
          using System.Text;
          using System.Globalization;
          using System.Web.Services.Protocols;
          using System.Threading;
          using System.CodeDom.Compiler;
          
          public class Pregen {
              public static int Main(string[] args) {
                  if (args.Length != 1) {
                      Console.WriteLine("usage: ");
                      Console.WriteLine("  pregen assembly");
                      return 1;
                  }
                  Pregen pregen = new Pregen();
                  return pregen.Run(args[0]);
              }
      
              int Run(string assemblyName) {
                  
                  try {
                      GenerateAssembly(assemblyName);
                  }
                  catch (Exception e) {
                      if (e is ThreadAbortException || e is StackOverflowException || e is OutOfMemoryException) {
                          throw;
                      }
                      Error(e, "Error: ");
                      return 1;
                  }
                  return 0;
              }
      
              void GenerateAssembly(string assemblyName) {
                  Assembly assembly = LoadAssembly(assemblyName, true);
                  Type[] types = assembly.GetTypes();
                  ArrayList mappings = new ArrayList();
                  ArrayList importedTypes = new ArrayList();
                  XmlReflectionImporter importer = new XmlReflectionImporter();
                  for (int i = 0; i < types.Length; i++) {
                      Type type = types[i];
                      if (HttpWebClientProtocol.GenerateXmlMappings(type, mappings)) {
                          importedTypes.Add(type);
                      }
                  }
                  if (importedTypes.Count > 0) {
                      Type[] serializableTypes = (Type[])importedTypes.ToArray(typeof(Type));
                      XmlMapping[] allMappings = (XmlMapping[])mappings.ToArray(typeof(XmlMapping));
                      
                      bool gac = assembly.GlobalAssemblyCache;
                      string codePath = gac ? Environment.CurrentDirectory : Path.GetDirectoryName(assembly.Location);
                      string serializerName = assembly.GetName().Name + ".XmlSerializers" ;
                      string location = Path.Combine(codePath, serializerName + ".dll");
      
                      CompilerParameters parameters = new CompilerParameters();
                      parameters.TempFiles = new TempFileCollection(codePath);
                      parameters.GenerateInMemory = false;
                      parameters.IncludeDebugInformation = false;
                      parameters.TempFiles = new TempFileCollection(codePath, true);
                      Assembly serializer = XmlSerializer.GenerateSerializer(serializableTypes, allMappings, parameters);
                      if (serializer == null) {
                          Console.Out.WriteLine("Failed pregenerate serializer for '{0}'", assembly.Location);
                      }
                      else {
                          AssemblyName name = serializer.GetName();
                          Console.Out.WriteLine("Serialization Assembly Name: {0}", name.ToString());
                          Console.Out.WriteLine("Generated serialization assembly for assembly {0} --> '{1}'.", assembly.Location, location);
                      }
                  }
                  else {
                      Console.Out.WriteLine("Assembly '{0}' does not contain any serializable types.", assembly.Location);
                  }
              }
      
              static Assembly LoadAssembly(string assemblyName, bool throwOnFail) {
                  Assembly assembly = null;
                  string path = Path.GetFullPath(assemblyName).ToLower(CultureInfo.InvariantCulture);
                  if (File.Exists(path)) {
                      assembly = Assembly.LoadFrom(path);
                  }
                  else {
                      try {
                          assembly = Assembly.Load(assemblyName);
                      }
                      catch (Exception e) {
                          if (e is ThreadAbortException || e is StackOverflowException || e is OutOfMemoryException) {
                              throw;
                          }
                          Error(e, "Error: ");
                      }
                      if (assembly == null) {
                          string justName = Path.GetFileNameWithoutExtension(assemblyName);
                          try {
                              assembly = Assembly.Load(justName);
                          }
                          catch (Exception e) {
                              if (e is ThreadAbortException || e is StackOverflowException || e is OutOfMemoryException) {
                                  throw;
                              }
                              Error(e, "Error: ");
                          }
                      }
                  }
                  if (assembly == null) {
                      if (throwOnFail)
                          throw new InvalidOperationException("Cannot load assembly " + assemblyName);
                      return null;
                  }
                  return assembly;
              }
      
              static void Error(Exception e, string prefix) {
                  Console.Error.WriteLine(prefix + e.Message);
                  if (e.InnerException != null) {
                      Error(e.InnerException, "  - ");
                  }
              }
      
              static void Warning(Exception e) {
                  Console.Out.WriteLine("  - " + e.Message);
                  if (e.InnerException != null) {
                      Warning(e.InnerException);
                  }
              }
          }
      }
    4. Add a reference to the System.Web.Services.dll file.
    5. Build the project.
    6. Copy the PreGen.exe file to the folder that contains the assembly that has the proxy code. For example, copy the PreGen.exe file to the assembly that contains the Web service client code.
    7. Pregenerate the serializers for the Web service by using the prepared PreGen.exe file. For example, if your proxy code is in the WindowsApplication1.exe file, run the following command at a command prompt:

      PreGen.exe WindowsApplication1.exe

      Note A .dll file is generated in the same folder where the client assembly is saved as soon as you run the command in step g.
    8. Open your client code in the Microsoft Visual Studio .NET IDE.
    9. In Solution Explorer, expand the Web Reference node. For example, if you added a reference to service1 by using localhost, expand LocalHost.
    10. Expand Reference.map, and then double-click the Reference.cs file.
    11. Locate the following code.

          [System.Diagnostics.DebuggerStepThroughAttribute()]
          [System.ComponentModel.DesignerCategoryAttribute("code")]
          [System.Web.Services.WebServiceBindingAttribute(Name="Service1Soap", Namespace="http://tempuri.org/")]
    12. Replace the code in step k with the following code.

      [System.Diagnostics.DebuggerStepThroughAttribute()]
      [System.ComponentModel.DesignerCategoryAttribute("code")]
      [System.Web.Services.WebServiceBindingAttribute(Name="Service1Soap", Namespace="http://tempuri.org/")]
      [System.Xml.Serialization.XmlSerializerAssemblyAttribute(CodeBase="<DLL Name>")]
    13. Rebuild your Web service client.

      Note You have to rebuild your client because of the attribute. Your client now has a run-time dependency on the assembly that contains the MySerializers.dll serializers. Therefore, you must deploy this assembly with your application and make the assembly available in the location where your client loads.


MORE INFORMATION

Assembly binding

The type of argument that you supply to the XmlSerializerAssemblyAttribute attribute determines the way that the serializer assembly is loaded. If the CodeBase argument is used, the serializer assembly will be loaded by using the LoadFrom method. If the AssemblyName argument is used, the serializer assembly will be loaded by using the LoadWithPartialName method. For more information about assembly loading, visit the following Microsoft Web site:

If you want to install your serializer assemblies to the Global Assembly Cache (GAC), you must use the AssemblyName argument, and supply the fully qualified assembly name.

Applications that are deployed by using Smart Client methods

If the assembly that contains the proxy is loaded and is executed from the local computer, it is preferable to use the CodeBase syntax for the XmlSerializerAssemblyAttribute attribute. In this case, the serializer assembly should be deployed to the same folder as the assembly that contains the proxy. If the assembly is loaded by using the LoadFrom method over HTTP in a Smart Client deployment scenario, for example, the serialization assembly will not be correctly loaded by the XmlSerializerAssemblyAttribute attribute. This problem occurs because the proxy assembly exists in the common language runtime loader's LoadFrom context instead of the Load context, and probing does not occur. To work around this problem, use the other AssemblyName syntax that is supported by the attribute with a dummy assembly name.

[System.Xml.Serialization.XmlSerializerAssemblyAttribute(AssemblyName="LoadMySerializerAssemblyNow")]

Your application can then implement a handler for the AppDomain's AssemblyResolve event to intercept the request to load the serializer assembly and load it from the correct location.

private void Form1_Load(object sender, System.EventArgs e)
{
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(MyResolveEventHandler);
}
static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
 {
            Assembly a = null;
            string serializationAssemblyPartialName = "LoadMySerializerAssemblyNow";
if (args.Name == serializationAssemblyPartialName)
    {string sSerializersDLL = "<Name Of The Serializer DLL>";
string smartDeploymentHostLocation = "<Full path location to the Serializer DLL>";
a = Assembly.LoadFrom(smartDeploymentHostLocation + sSerializersDLL);
}
            return a;
        }

Alternative pregenerator sample

The following is a more sophisticated version of the serializer pregenerator sample application. This version permits creating signed serializer assemblies. Use the following code.

namespace PreGenNS 
{
    using System;
    using System.Collections;
    using System.IO;
    using System.Reflection;
    using System.Xml.Serialization;
    using System.Text;
    using System.Globalization;
    using System.Web.Services.Protocols;
    using System.Threading;
    using System.CodeDom.Compiler;
    using System.Diagnostics;
    using System.Text.RegularExpressions;

    public class Pregen 
    {
        private static bool _verbose = true;

        
        // We use this as the standard suffix on all the serializer DLLs.
        // It must match the short name of the proxy assembly class with 
        const string SerializerSuffix = ".Serializer.dll";

        
        /// Key in the app config file with path of AssemblyInfo file.
        private const string AssemblyInfoAppKey = "AssemblyInfoFile";

        //private const string CSCCmd = "csc.exe";
        // Obtain the full path for the compiler, just in case the path is not set correctly

        private readonly string CSCCmd = System.Runtime.InteropServices.RuntimeEnvironment.RuntimeDirectory() + "csc.exe";

        public static int Main(string[] args) {     
            // Are we in a recursive call?
            //TODO: could really use a single value -- use filesToDelete...
            if (AppDomain.CurrentDomain.GetData(CallingAppDomainKey) == null) {
                return RunSlave(args);
            }

            string dllName = "";
            bool invalidUsage = false;

            if(args.Length == 1)
            {
                dllName = args[0];
            }
            else if(args.Length == 2)
            {
                if(!(args[0] == "/Q" || args[0] == "/q"))
                {
                    invalidUsage = true;
                }
                else
                {
                    dllName = args[1];
                    _verbose = false;
                }
            }
            else 
            {
                invalidUsage = true;
            }

            if (invalidUsage)
            {
                Console.WriteLine("usage: ");
                Console.WriteLine(" pregen [/Q] assembly");
                return 1;
            }
            

            // Update Private path setting in current application domain
            if (updatePrivatePath() != 0)
            {
                return 1;
            }
            
            Pregen pregen = new Pregen();
            int rc = pregen.Run(dllName);
            //Console.Read();
            return rc;
        }


        // Reads private path settings from config file and updates appdomain.  This permits configurable probing that is needed for preserialization.
        static int updatePrivatePath()
        {
            string defaultPrivatePath = System.Configuration.ConfigurationSettings.AppSettings["PregenDefaultPrivatePath"];
            string dynamicPrivatePath = System.Configuration.ConfigurationSettings.AppSettings["PregenDynamicPrivatePath"];
            string env_PREGEN_VALUES = Environment.GetEnvironmentVariable("PREGEN_VALUES");
            string [] replacementBlocks, temp;

            if (_verbose)
                Console.WriteLine("Read PREGEN_VALUES Env Variable, Value = " + env_PREGEN_VALUES);

            //process the dynamic path if the environment variable PREGEN_VALUES is present 
            if (env_PREGEN_VALUES == null || env_PREGEN_VALUES == "")
            {
                if (defaultPrivatePath != null && defaultPrivatePath != "")
                {
                    AppDomain.CurrentDomain.AppendPrivatePath(defaultPrivatePath);

                    if (_verbose)
                        Console.WriteLine("Appended private path with: " + defaultPrivatePath);
                }
            }
            else
            {
                if (dynamicPrivatePath != null && dynamicPrivatePath != "")
                {
                    //do substitutions in dynamic path
                    replacementBlocks = env_PREGEN_VALUES.ToUpper().Split(";".ToCharArray());
                    dynamicPrivatePath = dynamicPrivatePath.ToUpper();

                    for(int i = 0; i < replacementBlocks.Length; i++)
                    {
                        temp = replacementBlocks[i].Split("=".ToCharArray());
                        if(temp.Length != 2)
                        {
                            Console.Error.WriteLine("Invalid Environment Variable format - PREGEN_VALUES");
                            return 1;
                        }

                        dynamicPrivatePath = dynamicPrivatePath.Replace(temp[0],temp[1]);
                    }
                    
                    AppDomain.CurrentDomain.AppendPrivatePath(dynamicPrivatePath);

                    if (_verbose )
                        Console.WriteLine("Appended private path with: " + dynamicPrivatePath);
                }
            }
            return 0;
        }
        
        int Run(string assemblyName) 
        {

            try 
            {
                GenerateAssembly(assemblyName);
            }
            catch (Exception e) 
            {
                if (e is ThreadAbortException || e is StackOverflowException || e is OutOfMemoryException) 
                {
                    throw;
                }
                Error(e, "Error processing " + assemblyName + ": ");
                return 1;
            }
            return 0;
        }



        // Generates the serializer assembly for the proxy assembly specified
        void GenerateAssembly(string assemblyName) 
        {
            Assembly assembly = LoadAssembly(assemblyName, true);
            Type[] types = assembly.GetTypes();
            ArrayList mappings = new ArrayList();
            ArrayList importedTypes = new ArrayList();
            XmlReflectionImporter importer = new XmlReflectionImporter();

            
            //Obtain the imported serializable types
            for (int i = 0; i < types.Length; i++) 
            {
                Type type = types[i];
                if (HttpWebClientProtocol.GenerateXmlMappings(type, mappings)) 
                {
                    importedTypes.Add(type);
                }
            }
            if (importedTypes.Count <= 0) {
                Console.Out.WriteLine("Assembly '{0}' does not contain any serializable types.", assembly.Location);
                return;
            }
 
            {
                Type[] serializableTypes = (Type[])importedTypes.ToArray(typeof(Type));
                XmlMapping[] allMappings = (XmlMapping[])mappings.ToArray(typeof(XmlMapping));

                bool wasError = false;
                bool gac = assembly.GlobalAssemblyCache;
                string codePath = gac ? Environment.CurrentDirectory : Path.GetDirectoryName(assembly.Location);

                //adjust compiler params
                CompilerParameters parameters = new CompilerParameters();
                parameters.GenerateInMemory = false;
                parameters.IncludeDebugInformation = false;
                parameters.TempFiles = new TempFileCollection(codePath, true);
                
                //generate the serializer
                Assembly serializer = XmlSerializer.GenerateSerializer(serializableTypes, allMappings, parameters);
                if (serializer == null) {
                    Console.Out.WriteLine("Failed pregenerate serializer for '{0}'", assembly.Location);
                    wasError = true;
                }
                else 
                {
                    serializer = null;
                }

                // Determine whether there is an assemblyInfoFile in the config file.
                string assemblyInfoFile = System.Configuration.ConfigurationSettings.AppSettings[AssemblyInfoAppKey];
                if (assemblyInfoFile != null) { 
                    if (! File.Exists(assemblyInfoFile)) {
                        Console.WriteLine("ERROR: AssemblyInfo file: {0} does not exist.", assemblyInfoFile);
                        wasError = true;
                    }
                }

                if (!wasError) {
                    // Recompile the Serializer, same options, except to include the assemblyInfo file and
                    // adjust the output name.

                    // We have to find 
                    //      1. a .cs file (the serializer source)
                    //      2. a .cmdline file (the compiler options used)
                    // among the temp files from the first compile.
                    
                    string csFile       = null;
                    string cmdlineFile  = null;

                    foreach (string curFile in parameters.TempFiles ) {
                        string fileNameLC = curFile.ToLower();
                        if (fileNameLC.EndsWith(".cs") ) {
                            csFile = curFile;
                        }
                        else if (fileNameLC.EndsWith(".cmdline")) {
                            cmdlineFile = curFile;
                        }
                        //Do not care about the other files...
                    }
                    if (csFile == null || cmdlineFile == null) {
                        Console.WriteLine("Error: needed to rebuild, but cannot find either .cs or .cmdline file\n");
                        DeleteTempFiles(parameters);
                        return;
                    }

                    // So now we have found the file and the cmdline args.  We only need run the compiled application after 
                    // adjusting the parameters to include the AssemblyInfo file and to change the output.
                    
                    // Typical calling options to csc for this sequence are:
                    //      csc /noconfig @xxx.cmdline
                    // we'll change this to expand the contents of the cmdline file

                    // build the right name for the target serializer.
                    //TODO: we should be able to read the attribute from the proxy DLL and match our output
                    // to that name.
                    string serializerName = Path.GetDirectoryName(assembly.Location) + @"\" 
                                            + assembly.GetName().Name + SerializerSuffix;

                    string cmdLine = AdjustCmdLine(cmdlineFile, assemblyInfoFile, serializerName);

                    ProcessStartInfo ps = new ProcessStartInfo(CSCCmd, cmdLine);
                    ps.WindowStyle = ProcessWindowStyle.Hidden;
                    Process p = Process.Start(ps);
                    p.WaitForExit();
                    int rc = p.ExitCode;
                    if (rc > 0) {
                        //TODO: put useful handling here...
                        Console.WriteLine("ERROR: Compiler problem, rc = {0}", rc);
                        wasError = true;
                    }

                    //TODO: Cannot ditch temp assembly because the assembly is now loaded.
                    DeleteTempFiles(parameters);

                    if (!wasError) {
                        Console.Out.WriteLine("Generated Serialization Assembly Name: {0}", serializerName);
                    }
                    Console.Out.WriteLine("Done");
                }
            }
        }


        // Delete temporary files from a CompilerParameters list.
        private void DeleteTempFiles(CompilerParameters cp) {
            ArrayList unDeletedFiles = new ArrayList(10);
            foreach(string fileName in cp.TempFiles) {
                try {
                    File.Delete(fileName);
                }
                catch(Exception) {
                    unDeletedFiles.Add(fileName);
                    //Console.WriteLine("Warning: Unable to delete temp file: {0}, exception={1}(\"{2}\")",
                    //  Path.GetFileName(fileName), e.GetType().FullName, e.Message);
                }
            }
            if (unDeletedFiles.Count > 0) {
                // put the list into the calling appDomain's environment for later deletion
                string[] files = new string[unDeletedFiles.Count];
                unDeletedFiles.CopyTo(files);

                //TODO: should really be concatenating to any existing value -- maybe leave as an ArrayList?
                AppDomain.CurrentDomain.SetData(FilesToDeleteKey, files);
            }
        }


        /// Rebuild a commandline for csc, adding assemblyInfoFile to the end of the line and adjusting the 
        /// output file name as specified.
        private string AdjustCmdLine(string cmdlineFile, string assemblyInfoFile, string outputFile) {  
            // Obtain the text from the @ response file that is used by the Framework Serialization builder
            StreamReader file = File.OpenText(cmdlineFile);
            string cmdLine = file.ReadToEnd();
            file.Close();

            // add the assemblyInfo file at the end of the command if it was specified
            if (assemblyInfoFile != null)
                cmdLine = String.Format(@"/noconfig {0} ""{1}""", cmdLine, assemblyInfoFile);

            // replace the /OUT option with our value.
            Regex re = new Regex(@"/OUT:""[^""]+.", RegexOptions.IgnoreCase);
            cmdLine = re.Replace(cmdLine, @"/OUT:""" + outputFile + @"""");

            return cmdLine;
        }

        static Assembly LoadAssembly(string assemblyName, bool throwOnFail) 
        {
            Assembly assembly = null;
            string path = Path.GetFullPath(assemblyName).ToLower(CultureInfo.InvariantCulture);
            if (File.Exists(path)) 
            {
                assembly = Assembly.LoadFrom(path);
            }
            else 
            {
                try 
                {
                    assembly = Assembly.Load(assemblyName);
                }
                catch (Exception e) 
                {
                    if (e is ThreadAbortException || e is StackOverflowException || e is OutOfMemoryException) 
                    {
                        throw;
                    }
                    Error(e, "Error: ");
                }
                if (assembly == null) 
                {
                    string justName = Path.GetFileNameWithoutExtension(assemblyName);
                    try 
                    {
                        assembly = Assembly.Load(justName);
                    }
                    catch (Exception e) 
                    {
                        if (e is ThreadAbortException || e is StackOverflowException || e is OutOfMemoryException) 
                        {
                            throw;
                        }
                        Error(e, "Error: ");
                    }
                }
            }
            if (assembly == null) 
            {
                if (throwOnFail)
                    throw new InvalidOperationException("Cannot load assembly " + assemblyName);
                return null;
            }
            return assembly;
        }

        static void Error(Exception e, string prefix) 
        {
            Console.Error.WriteLine(prefix + e.Message);
            if (e.InnerException != null) 
            {
                Error(e.InnerException, " - ");
            }
        }

        static void Warning(Exception e) 
        {
            Console.Out.WriteLine(" - " + e.Message);
            if (e.InnerException != null) 
            {
                Warning(e.InnerException);
            }
        }

        private static AppDomain DuplicateAppDomain(AppDomain template, string newName) {
            AppDomain res = AppDomain.CreateDomain(newName, template.Evidence, template.SetupInformation);
            return res;
        }

        // keys in AppDomain properties
        private const string CallingAppDomainKey    = "__Calling_AppDomain__";
        private const string FilesToDeleteKey       = "__Files_To_Delete__";

        // Called from Main to set up and run the second copy of the program.
        // args: command-line arguments for second execution
        private static int RunSlave(string[] args) {

            // Start a copy of this program in another application domain
            AppDomain ad = DuplicateAppDomain(AppDomain.CurrentDomain, "serializerAD");
            Assembly ca = Assembly.GetExecutingAssembly();

            // set a marker so target domain knows that it is the subordinate.
            ad.SetData(CallingAppDomainKey, AppDomain.CurrentDomain.FriendlyName);
            ad.SetData(FilesToDeleteKey, new string[0]);

            int rc = ad.ExecuteAssembly(ca.Location, ca.Evidence, args);

            // Now delete any files
            string[] fileList = (string[])ad.GetData(FilesToDeleteKey);

            AppDomain.Unload(ad);

            if (fileList != null) {
                foreach(string fileName in fileList) {
                    try {
                        File.Delete(fileName);
                    }
                    catch(Exception e) {
                        Console.WriteLine("Warning: Unable to delete temp file: {0}, exception={1}(\"{2}\")",
                            Path.GetFileName(fileName), e.GetType().FullName, e.Message);
                    }
                }
            }
            return rc;  
        }

    }
}

Keywords: kbqfe kbhotfixserver kbtshoot kbdll kbwebservices kbuser kbarchitecture kbfix KB872800