John McFadyen's profileJohn McFadyens Windows I...PhotosBlogGuestbookMore Tools Help

Blog


    May 07

    Understanding the Windows Installer Logs

    When people are beginning to use Windows Installer the first look at the logs is often so daunting they tend to never look at them again. I find that the most common reason people avoid the logs is they don't understand the sequencing. Funnily enough the sequences and the logs tend to correspond with each other (I'm not sure why that is)

    Anyway if you take Windows Installer and break it down you would know that the Installer basically consists of a service and a database that processes table's then sequences and actions. The installer service records everything that happens in sequential logic. The end result is a transactional record of the actions that are run during the installation. The beauty of this is as its transactional its quite simple to apply the reverse logic to handle the uninstall.

    Now the trusty old log files record this process in its entirety, basically what I am getting at here is if you take the time to actually read the logs you would also understand the sequencing logic as well because they are one in the same. Alternatively if you work out the sequences you also workout the logs.

    So first things first with the logs. How do we enable Windows Installer logging ?

    Before we enable logging there is a number of available logging options which you need to know, there are:

    Logging Option Option Description
    v Verbose output
    o Out of disk space messages
    i status messages
    c Initial UI Parameters
    e All error messages
    w Non fatal warnings
    a start up of actions
    r action specific records
    m Out of memory or fatal exit information
    u user requests
    p terminal properties
    + Append to existing file
    ! Flush each line to the log
    x Extra logging (Version 3.0 up, Windows 2003 + OS's)

    Note: Each letter corresponds to a different setting. Adding a collection of all letters increases logging capabilities.

    Logging via Registry

    System Key: [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Installer]
    Value Name: Logging
    Data Type: REG_SZ (String Value)

    image

    Note: A common acronym of VOICEWARMUP is used as its a word that contains all available settings (prior to V3.0)

    Debugging via Registry

    System Key: [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Installer]
    Value Name: Debug
    Data Type: REG_DWORD

    image

    By enabling the debug key you can monitor the installation using debugview

    Install script logging

    System Key: [HKEY_CURRENT_USER\Software\InstallShield\ISWI\3.0\SetupExeLog]
    Value Name: VerboseLogFileName
    Data Type: REG_SZ

    Group Policy Logging

    To enable logging from active directory or GPO you can find the settings here.

    image

    Windows Installer 4.0 +

    So then we got Windows Installer 4.0 from which came two new Windows Installer properties.

    MSILogging property hosts the same values listed in the above table.

    MSILogFileLocation property has the path to where the logs should be created.

    Command Line Logging

    So last but not least command line logging, probably the one you will most often use.

    msiexec.exe /i <path to msi> /l*v <path to logfile>

    msiexec.exe /i <path to msi> /l*vx <path to logfile> (version 3.0 on Windows 2003 + OS's)

    Reading the logs

    Ok so now you have the logs how do we interpret the garbage that it generates. Well for those of you who always like to take the easy road. Get your hands on this little utility WILOGUTL.EXE it makes pretty it all a little more legible and takes the guess work out of things.

    However for all of you hard core propeller heads here's how to really understand the seemingly difficult mess that arrive after setting some of the above options to log.

    As you are aware the Installer Service on WindowsNT based platforms offers a client and server based process. The first section depicted in the following log excerpt in blue identifies the process is running client or server side.

    MSI (s) (DC:D8) [22:18:04:544]: MainEngineThread is returning 1603
    MSI (c) (34:30) [22:18:05:544]: Back from server. Return value: 1603

    MSI (S) is server side

    MSI (C) is client side

    MSI (N) is a nested action

    The next items depicted above in the red text is the ProcessID:ThreadID that generated the entry. Only the last 2 (hexadecimal) digits of the process and thread IDs are given. If the process ID was 2DC and thread ID 2D8 the Hex item show in the log would be (DC:D8).

    The green section following is the date time stamp the action occurred.

    Product State

    The ProductState property can be any one of these values

    Value Description
    -1 The product is neither advertised nor installed.
    1 The product is advertised but not installed
    2 The product is installed for a different user
    5 The product is installed for the current user

    Feature Component State

    So every feature and component is checked for state prior to it being marked as something that needs to be installed. So selecting different features "usually" changes the feature state and subsequently all the components within that feature (of course there are exceptions to every rule, the condition table and feature / component conditions all have effect here also.).

    The first item here, show the feature name as Complete and it is currently Absent. So the requested action is to install it Local. So the components follow the same pattern. This one is a little tricky so more detail here

     

    Value Value:        Description
    Installed Local        
    Source     
    Advertise  
    Absent      
    already installed to run local
    already installed to run from source
    already installed as advertised
    not installed
    Request Local
    Source     
    Advertise  
    Reinstall   
    Absent     
    requests to install the item to run local
    requests to install the item to run from source
    requests to advertise the item
    requests to reinstall the item
    should not be installed
    Action Local        
    Source     
    Reinstall   
    Absent     
    Null         
    actually performs install to run local
    actually performs install to run from source
    actually reinstalls the item
    actually removes the item
    do nothing

    MSI (s) (94:28) [20:46:12:390]: Feature: Complete; Absent: Local;   Request: Local;   Action: Local
    MSI (s) (94:28) [20:46:12:390]: Component: Registry; Installed: Absent;   Request: Local;   Action: Local
    MSI (s) (94:28) [20:46:12:390]: Component: VersionRegistry; Installed: Local;   Request: Local;   Action: Local
    MSI (s) (94:28) [20:46:12:390]: Component: slupExecutable; Installed: Absent;   Request: Local;   Action: Local
    MSI (s) (94:28) [20:46:12:390]: Component: CoreLibrary; Installed: Absent;   Request: Local;   Action: Local
    MSI (s) (94:28) [20:46:12:390]: Component: CtrlLibrary; Installed: Absent;   Request: Local;   Action: Local

    Reading Actions and return codes

    Action start 1:18:02: INSTALL.
    MSI (c) (A1:6A): UI Sequence table 'InstallUISequence' is 
    present and populated.
    MSI (c) (A1:6A): Running UISequence
    MSI (c) (A1:6A): Skipping action: ResumeInstall (condition is false)
    MSI (c) (A1:6A): Doing action: CheckOSandSPAction
    Action start 1:18:02: CheckOSandSPAction.
    MSI (c) (A1:6A): Creating MSIHANDLE (1) of type 790542 for thread 110
    Action ended 1:18:02: CheckOSandSPAction. Return value 1.
    The valid return codes are: 
    
    Value Description
    0

    Action not invoked; most likely does not exist.

    1

    Completed actions successfully.

    2

    User terminated prematurely.

    3

    Unrecoverable error occurred.

    4

    Sequence suspended, to be resumed later.

    Shell32 API calls

    Where you see one of these there is a good chance a directory is being resolved for the current target platform.

    MSI (s) (94:28) [20:46:11:843]: SHELL32::SHGetFolderPath returned: C:\Documents and Settings\All Users\Templates

    Old hands tips for log reading

    1) Start at the bottom and work up

    2) If your install fails with dialogs open. Leave dialogs on screen and goto log whilst error message is still visible

    3) Use WiLogUtl.exe

    4) Don't forget the newest extended logging options /l*vx (most of you are still using /l*v)

    5) Keep the sequences in mind when reading a logfile (strangely enough they coincide nicely)

    6) Enable GPO logging or policy registry option if you need to log repairs

    7) Now you have this information take a 30 minutes out of your day and read an entire log you'll be surprised what you learn.

    Now all that being said, I'm not getting a lot of feedback about these blogs so its hard to gauge if anyone is really reading or even interested in this stuff or not.

    Post some comments if you want more info otherwise I may just find other things to do with my spare time.

    May 05

    Custom XML Dom Class

    Technorati Tags: ,,,

    So lately a bunch of people are asking a little more about XML and XML DOM. Sometime ago when I was trying to learn how XML dom worked I threw this vbs class together to assist in reading and writing xml via the DOM. For those of you whom are not aware of the dom its the XML Document Object Model.

    Using the DOM you can traverse xml nodes read and manipulate data. Looking at this entirely from a packaging perspective its a great way to store and read package data into a package.

    So the attached script emulates the functionality in the NetFX classes, something I was not aware of prior to writing this code. So here is a bunch of vbs function (or methods for the dev's out there). Using these methods you can add / get / set and delete xml data from an XML structure. There is even a simple method to format the white space for readability purposes.

    There is a bunch of sample code at the top of the script to aid in how this should be used. If anyone is after some more specific requests in how they can pull data from xml drop a few comments and I will endeavour to answer your questions.

    The code is pretty old but its a great starting point for those of you whom are interested in learning xml.

    '============================================================================
    ' LANG:            VBScript
    ' NAME:            IpXMLClass.vbs
    ' AUTHOR:        John McFadyen (john.mcfadyen@gmail.com)
    ' VERSION:        0.1
    ' DATE:            2006-07-30
    ' Description:        Generate / edit / query an XML file
    '
    ' UPDATES:        http://www.installpac.com/scripting
    '
    ' For Options:        This script has no options and is expected to be used as a class.
    ' Feedback:        Please send feedback to john.mcfadyen@gmail.com
    '
    ' Notes:
    ' This has has only been tested with MSXML
    ' LICENSE:
    ' Copyright (c) 2004-2006, John McFadyen
    ' All rights reserved.
    '
    ' Redistribution and use in source and binary forms, with or without
    ' modification, are permitted provided that the following conditions are met:
    '
    '  * Redistributions of source code must retain the above copyright notice,
    '    this list of conditions and the following disclaimer.
    '  * Redistributions in binary form must reproduce the above copyright notice,
    '    this list of conditions and the following disclaimer in the documentation
    '    and/or other materials provided with the distribution.
    '  * Neither the name Installpac or the names of its contributors may be used
    '    to endorse or promote products derived from this software without
    '    specific prior written permission.
    '
    ' THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    ' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    ' IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ' ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
    ' LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    ' CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    ' SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    ' INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    ' CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    ' ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    ' POSSIBILITY OF SUCH DAMAGE.
    '==========================================================
    '==========================================================

    ' Example 1 Generating a new xml document
    ' =======================================
    ' set xmlDoc = new XMLDom
    ' xmlDoc.filenameXML = "Johnny.xml"
    ' call xmldoc.CreateElement("Root")
    '    call xmldoc.CreateElement("License")
    '        call xmldoc.CreateElementString("starttime", now())
    '        call xmldoc.CreateElementString("starttime", now())
    '        call xmldoc.CreateElementString("starttime", now())
    '        call xmldoc.CreateElementString("starttime", now())
    '        call xmldoc.CreateElement("Hello")
    '        call xmldoc.CreateEndElement
    '        call xmldoc.CreateElement("Hello1")
    ' call xmldoc.close
    '
    ' Example 2 Updating a current xml
    ' ================================
    ' set xmlDoc = new XMLDom
    ' xmlDoc.filenameXML = "Y:\Projects\TestApplication_1-0_R01\TestApplication_1-0_R011New.XML"
    ' xmlDoc.ValidateXML "Y:\Projects\TestApplication_1-0_R01\TestApplication_1-0_R011.XML"
    ' xmlDoc.XMLLoader "Y:\Projects\TestApplication_1-0_R01\TestApplication_1-0_R011.XML"
    ' Note: The following line using an external object
    ' set objNode = xmlDoc.GetNode("//PACKAGESRC")
    ' xmlDoc.SetNodeValue "Test", objNode, false
    ' set objNodeLocation = xmlDoc.GetNode("//LOCATION")
    ' xmlDoc.CreateElementString "NewNode", "test"
    ' xmlDoc.close

    ' Example 3 Using an actual stylesheet
    ' =========
    ' call xmldoc1.CreateElement("Root")
    ' call xmldoc1.CreateXmlProcess("stylesheet.xsl")
    '     call xmldoc1.CreateElement("License")
    '    call xmldoc1.CreateElementString("starttime", now())
    '     call xmldoc1.CreateElementString("starttime", now())
    '    call xmldoc1.CreateElementString("starttime", now())
    '     call xmldoc1.CreateElementString("starttime", now())
    '     call xmldoc1.CreateAttribute("test", "value1")
    '    call xmldoc1.CreateAttribute("test1", "value1")
    '     call xmldoc1.CreateAttribute("test2", "value1")
    ' call xmldoc1.CreateEndElement
    ' call xmldoc1.close
    '
    '=========================================================================================================
    '=========================================================================================================
    ' Script version

    Dim strScriptVersion
    strScriptVersion = "0.1"

    '=========================================================================================================

    class XMLDom

        dim xmlfilename                        'stores the xml filename
        dim xslfilename                        'stores the xsl filename
        dim xmldom                          'stores a DOM Object
        dim xmlroot                            'stores the Root Node
        dim xmlcurrentnode                    'stores the Current Node
        dim xmlparent                        'stores the Parent Node of current node
        dim xmlelements
        dim intIndentation                    'stores the indentspacing (not required with XSL sheet applied)
        dim blnWhiteSpace

    '=========================================================================================================

        private Sub Class_Initialize
            dim xmldeclaration
            Set xmlDom = CreateObject("Microsoft.XMLDOM")
            xmlDom.preserveWhiteSpace = False
            xmlDom.loadxml "<?xml version='1.0'?>"
            'xmlDom.createProcessingInstruction("<?xml version='1.0'?>")
            set xmlcurrentnode = xmlDom
        End Sub

        private Sub Class_Terminate
            'xmldom.LoadXML TransformXML
            'if xmlFilename <> nothing then xmldom.save(xmlFilename)
        End Sub

    '=========================================================================================================

        public Property let FilenameXML(strFilename)   
            xmlFilename = strFilename
        End Property

        public Property let FilenameXSL(strFilename)   
            xslFilename = strFilename
        End Property

        public Property let Indentspacing(intIndentSpacing)
            ' Not required now we are using stylesheets
        End Property

        public Property let Whitespace(blnWhiteSpace)
            sprpWhiteSpace = blnWhiteSpace
        End Property

        public Property get Whitespace()
             sprpWhiteSpace = prpWhiteSpace
        End Property

        public Property let EmptyAttribute(blnAllowEmpty)
            prpAllowEmpty = blnWhiteSpace
        End Property

    '=========================================================================================================

        public function CreateRootElement(strRootNode)

            set xmlroot = xmldom.CreateElement(strRootNode)
            xmldom.appendchild xmlroot
            set xmlcurrentnode = xmlroot
            Set objProcessing = xmlDom.CreateProcessingInstruction("xml","version='1.0'")
            xmlDom.insertBefore objProcessing ,xmlroot
            'xmlroot.appendChild xmlDom.CreateTextNode(vbcrlf & string(indentLevel, Chr(9)))   

        end function

        sub CreateElement(strElementName)
            dim xmlNewNode
            set xmlNewNode = xmlDom.createElement(strElementName)
            xmlcurrentnode.appendchild xmlNewNode
            set xmlcurrentnode = xmlNewNode
            'xmldom.save xmlFilename
            'xmlcurrentnode.appendChild xmlDom.CreateTextNode(vbcrlf & string(indentLevel, Chr(9)))   
        end sub

        sub CreateElementString(strElementName, strElementValue)
            dim xmlNewNode
            set xmlNewNode = xmlDom.createElement(strElementName)
            if isnull(strElementValue) then strElementValue = ""
            xmlNewNode.text = strElementValue
            xmlcurrentnode.appendchild(xmlNewNode)
            'xmlcurrentnode.appendChild xmlDom.CreateTextNode(vbcrlf & string(indentLevel, Chr(9)))   
        end sub

        sub CreateElementStringNew(strElementName, strElementValue)
            dim xmlNewNode
            set xmlNewNode = xmlDom.createElement(strElementName)
            if isnull(strElementValue) then strElementValue = ""
            xmlNewNode.text = strElementValue
            xmlcurrentnode.appendchild(xmlNewNode)
            'xmlcurrentnode.appendChild xmlDom.CreateTextNode(vbcrlf & string(indentLevel, Chr(9)))   
        end sub

        sub CreateEndElement
            set xmlcurrentnode = xmlcurrentnode.parentnode
        end sub

        sub DeleteEndElement
            dim xmlEndNode
            set xmlEndNode = xmlcurrentnode
            set xmlcurrentnode = xmlcurrentnode.parentnode
            xmlcurrentnode.removechild(xmlEndNode)
        end sub

        Function RemoveElement(strNodename, objNode)
            Dim objRemoveElement    ' As MSXML2.IXMLDOMNode   
            If Not objNode Is Nothing Then
              Set objRemoveElement = objNode.SelectSingleNode(strNodename)   
              If Not objRemoveElement Is Nothing Then
                objNode.removeChild objRemoveElement
              End If
            End If
        End Function

        Public Function RemoveElements(sXPath, objNode)
            Dim objNodes            ' As MSXML2.IXMLDOMNodeList
            Dim intNodeCount        ' As Long
            Set objRemoveElements = objNode.selectNodes(sXPath)
            if not objRemoveElements is nothing then
              For intNodeCount = 0 To objRemoveElements.length - 1
                objRemoveElements(intNodeCount).parentNode.removeChild objRemoveElements(intNodeCount)
              Next
            end if
        End Function

        Public Function GetElements

            set xmlElements = xmlDom.DocumentElement
            set GetElements = xmlElements
        end function

    '=========================================================================================================

        sub CreateAttribute(strAttrName,strAttrValue)
            if isnull(strAttrValue) then strAttrValue = ""
            xmlcurrentnode.setAttribute strAttrName, strAttrValue
        end sub

        Public Function GetAttribute(strAttrName, objNode)     ' as string
            Dim objAttr                                            ' As MSXML2.IXMLDOMNode
            GetAttribute = ""
            If Not objNode Is Nothing Then
              Set objAttr = objNode.Attributes.getNamedItem(strAttrName)
              If Not objAttr Is Nothing Then
                GetAttribute = CStr(objAttr.Text)
              End If
            End If
        End Function
        Public Function SetAttribute(strAttrName, strAttrValue, objNode)                     
            Dim objAttr      ' As MSXML2.IXMLDOMNode
            If Not objNode Is Nothing Then
              If strAttrValue <> "" or AllowEmptyAttr Then
                Set objAttr = objNode.Attributes.getNamedItem(strAttrName)
                If objAttr Is Nothing Then
                  Set objAttr = CreateAttribute(strAttrName, strAttrValue, objNode)
                End If 
                objAttr.Text = strAttrValue
              Else
                RemoveAttribute strAttrName, objNode
              End If
            End If       
        End Function

        Function RemoveAttribute(strAttrName, objNode)
            If Not objNode Is Nothing Then
              objNode.Attributes.removeNamedItem strAttrName
            End If
        End Function 
    '=========================================================================================================

        Public Function HasChildNodes()
            HasChildNodes = False
            if xmlCurrentNode.HasChildNodes then HasChildNodes = True

        end function

        Public Function NodeExists(strNodeName)
            Dim objNode
            msgbox "node test"
            NodeExists = false
    '        if xmlCurrentNode.HasChildNodes then
            For each objNode in xmlCurrentNode.ChildNodes
                    msgbox objNode.Nodename
                    if objNode.Nodename = strNodename then
                        NodeExists = True
                        exit for
                    end if
                next
    '        end if
        end function

        Public Function GetChildNodes()

            'On Error Resume Next

            Dim arrNodes()
            redim preserve arrNodes(1,0)
            y = 0
            For each objNode in xmlCurrentNode.ChildNodes
                if y >= ubound(arrNodes,2) then redim preserve arrNodes(1,y)
                arrNodes(0,y) = objNode.Nodename
                arrNodes(1,y) = objNode.Text
                y = y + 1
            Next
            GetChildNodes = arrNodes

        End Function

        Public Function GetNode(sXpath)
            on error resume next
            'Note: requires GetElements to be run first
            '    : preffered to get to external object as well.
            '    : I.E. set objNode = GetNode("//Nodename")

            if not xmlelements then
                GetElements
            end if
            set xmlcurrentnode = xmlElements.selectSingleNode(sXPath)
            Set GetNode = xmlcurrentnode

        end function

        Public Function GetNodeValue(objNode, blnCData)    ' As String
            on error resume next
            if not xmlelements then
                GetElements
            end if

            set xmlcurrentnode = xmlElements.selectSingleNode(sXPath)

            If Not xmlcurrentnode Is Nothing Then
              If blnCData Then
                If xmlcurrentnode.childNodes.length > 0 Then
                  GetNodeValue = CStr(xmlcurrentnode.childNodes(0).nodeTypedValue)
                End If
              Else
                GetNodeValue = CStr(xmlcurrentnode.Text)
              End If
            End If
        End Function

        Public Function SetNodeValue(strNodevalue, objNode, blnCData)    ' As String
            on error resume next
            If Not objNode Is Nothing Then
              If blnCData Then
                If objNode.childNodes.length > 0 Then
                  objNode.childNodes(0).nodeTypedValue = sValue
                Else
                  objNode.appendChild objNode.ownerDocument.createCDATASection(strNodevalue)
                End If
              Else
                objNode.Text = strNodevalue
              End If
            End If
        End Function
    '=========================================================================================================

        sub CreateXmlProcess (strProcessInstruction)
            dim xmlProcessInstruction
            Set xmlProcessInstruction = xmlDom.createProcessingInstruction(strProcessInstruction)
            xmlDom.appendChild(xmlProcessInstruction)
        end sub

        sub CreateXSlProcess (strProcessInstruction)
            dim xmlProcessInstruction
            Set xmlProcessInstruction = xmlDom.createProcessingInstruction("xml-stylesheet", "type=""text/xsl"" href="""& strProcessInstruction & """")
            xmlDom.appendChild(xmlProcessInstruction)
        end sub

        Public Sub StartInsertAt(objParentNode)
            Set xmlcurrentnode = objParentNode
        End Sub

        function LoadXML(strxml)
            xmldom.loadXML(strxml)
        end function

        public function XMLLoader(strXML)
            xmldom.load(strXML)
        end function

        Public Function validateXML(strXML)

            Dim sReturn
            xmldom.async = 0
            xmldom.resolveExternals = 1
            xmldom.validateOnParse = 1
            If xmldom.load(strXML) Then
            Else
                If xmldom.parseError.errorCode <> 0 Then
                    strReturn = xmldom.parseError.reason & vbcrlf & _
                        xmldom.parseError.line & vbcrlf & _
                        xmldom.parseError.linepos & vbcrlf & _
                        xmldom.parseError.srcText
                End If
            End IF
            ValidateXML = strReturn

        End Function

        Public Function transformXML1(strXML,strXSL)

            Dim xslDoc
            Set xslDoc = New MSXML2.DOMDocument40

            If xmlDom.parseError.errorCode = 0 Then
                xslDom.Load strXSL
                If xslDoc.parseError.reason = "" Then
                    return = xmlDom.transformNode(xslDoc)
                Else
                    return = "Error: Stylesheet.xsl did not load. " & _
                    xslDoc.parseError.reason
                End If
            Else
                return = "Error: XML did not load. " & _
                xmlDoc.parseError.reason
            End If
            transformXML1 = return
            Set xslDoc = Nothing

        End Function

        function TransformXML
            dim objStylesheet
            set objStylesheet = CreateObject("Microsoft.XMLDOM")
            objStylesheet.async = False
            objStylesheet.loadXML ("<?xml version=""1.0"" encoding=""UTF-8""?>" & vbcrlf & _
                            "<xsl:stylesheet version=""1.0"" xmlns:xsl=""http://www.w3.org/1999/XSL/Transform"">" & vbcrlf & _
                            "<xsl:output method=""xml"" indent=""yes"" encoding=""UTF-8"" />" & vbcrlf & _
                            "<xsl:template match=""@* | node()"">" & vbcrlf & _
                            "   <xsl:copy>" & vbcrlf & _
                            "        <xsl:apply-templates select=""@* | node()"" />" & vbcrlf & _
                            "   </xsl:copy>" & vbcrlf & _
                            "</xsl:template>" & vbcrlf & _
                            "</xsl:stylesheet>")
            'objStyleSheet.Save "c:\test.xsl"
            TransformXML = xmlDOM.transformNode(objStylesheet)
        end function

    '=========================================================================================================

        sub close()
            xmldom.LoadXML TransformXML
            xmldom.save(xmlfilename)
        end sub
    end class