Agregar nodos a xml con DOM en inno Setup - problemas extraños

Problema muy extraño: uso el DOM para editar un archivo xml (un archivo .exe.config para una aplicación que necesita interactuar con la nuestra), pero como tengo que agregar varias secciones similares en forma masiva, hice una función para insertar Todo el bloque necesario.

Llamar a esta función una vez funciona perfectamente. Llamarlo nuevamente con diferentes parámetros justo después da una excepción (vea la explicación debajo del código).

El código:

// Split a string into an array using passed delimeter
procedure Explode(var Dest: TArrayOfString; Text: String; Separator: String);
var
  i: Integer;
begin
  i := 0;
  repeat
    SetArrayLength(Dest, i+1);
    if Pos(Separator,Text) > 0 then
    begin
      Dest[i] := Copy(Text, 1, Pos(Separator, Text)-1);
      Text := Copy(Text, Pos(Separator,Text) + Length(Separator), Length(Text));
      i := i + 1;
    end
    else
    begin
      Dest[i] := Text;
      Text := '';
    end;
  until Length(Text)=0;
end;

// Ensures an XPath exists, creating nodes if needed
function EnsureXPath(const XmlDoc: Variant; XPath: string): Variant;
var
  PathParts: TArrayOfString;
  TestNode, CurrentNode, NewNode: Variant;
  NodeList: Variant;
  i, j: Integer;
  found: Boolean;
begin
  CurrentNode:=XMLDoc.documentElement;
  Explode(PathParts, XPath, '/');

  MsgBox('Array length: ' + IntToStr(GetArrayLength(PathParts)), mbInformation, MB_OK);
  for i := 0 to GetArrayLength(PathParts) - 1 do
  begin
    MsgBox('Current path part:'#13#10 + '''' + pathparts[i] + '''', mbInformation, MB_OK);
    if pathparts[i] <> '' then
    begin
      //MsgBox('Current node:'#13#10 + '''' + CurrentNode.NodeName + '''' + #13#10'Current path part:'#13#10 + '''' + PathParts[i] + '''' + #13#10'List length: ' + IntToStr(NodeList.Length), mbInformation, MB_OK);
      MsgBox('Current node:'#13#10 + '''' + CurrentNode.NodeName + '''', mbInformation, MB_OK);
      MsgBox('Current path part:'#13#10 + '''' + PathParts[i] + '''', mbInformation, MB_OK);
      NodeList:= CurrentNode.childNodes;
      MsgBox('List length: ' + IntToStr(NodeList.Length), mbInformation, MB_OK);
      found:=false;
      for j := 0 to NodeList.Length - 1 do
      begin
        TestNode:=NodeList.Item[j]
        if (TestNode.NodeName = pathparts[i]) then
        begin
          currentNode:= TestNode;
          found:=true;
        end;
      end;      
      if (not found) then
      begin
        newNode := XMLDoc.createElement(PathParts[i]);
        currentNode.appendChild(newNode);
        currentNode:=currentNode.lastChild;
      end;
    end;
  end;
  Result:=currentNode;
  MsgBox('Last node:'#13#10 + '''' + CurrentNode.NodeName + '''', mbInformation, MB_OK);
end;

// Seeks out a node, returning the node in "resultnode", and whether it was found as Result.
function SeekNode(const ParentNode: Variant; var resultnode: Variant; subNodePath, attrName, attrValue :String; IsFirstCall: Boolean): Boolean;
var
  NodesList: Variant;
  AttrNode: Variant;
  AttrList: Variant;
  Attr: Variant;
  PathParts, NewPathParts: TArrayOfString;
  i, j, truelength: Integer;
  currentPath, remainderPath: String;
  CallAgain,callResult: Boolean;
begin
  Explode(PathParts, subNodePath, '/');
  truelength:=GetArrayLength(PathParts);
  for i:=0 to GetArrayLength(PathParts) -1 do
  begin 
    if PathParts[i] = '' then
      truelength:=truelength-1;
  end;
  if (truelength <> GetArrayLength(PathParts)) then
  begin
    SetArrayLength(NewPathParts, truelength);
    truelength:=0;
    for i:=0 to GetArrayLength(PathParts) -1 do
    begin 
      if PathParts[i] <> '' then
      begin
        NewPathParts[truelength] := PathParts[i];
        truelength:=truelength+1;
      end;
    end;
  end
  else
    NewPathParts:=PathParts;

  CallAgain:=GetArrayLength(NewPathParts)>1;
  currentPath:=NewPathParts[0];
  remainderPath:='';
  for i:=1 to GetArrayLength(NewPathParts) -1 do
  begin
    if (remainderPath <> '') then
      remainderPath:=remainderPath + '/';
    remainderPath:=remainderPath + NewPathParts[i];
  end;
  NodesList:=ParentNode.childNodes;
  //MsgBox('Node count for ' + currentPath + ':'#13#10 + '''' + IntToStr(NodesList.length) + '''', mbInformation, MB_OK);
  Result:=false;
  for i := 0 to NodesList.length - 1 do
  begin
    attrNode := NodesList.Item[i];
    //MsgBox('Current node:'#13#10 + '''' + attrNode.NodeName  + ''''#13#10'Current path:'#13#10+ '''' + currentPath  + '''', mbInformation, MB_OK);
    if (attrNode.NodeName = currentPath) then
    begin
      if CallAgain then
      begin
        //MsgBox('Remainder of path:'#13#10 + '''' + remainderPath  + '''', mbInformation, MB_OK);
        callResult:=SeekNode(attrNode, resultnode, remainderPath, attrName, attrValue, false);
        if callResult then
        begin
          Result:=true;
          if IsfirstCall then
            resultnode:=attrNode;
          exit;
        end;
      end
      else
      begin
        AttrList:=attrNode.Attributes;
        //MsgBox('Node:'#13#10 + '''' + attrNode.NodeName + '''' + #13#10'Attributes count:'#13#10 + '''' + IntToStr(AttrList.Length) + '''', mbInformation, MB_OK);
        for j := 0 to AttrList.length - 1 do
        begin
          Attr:= AttrList.Item[j];
          //MsgBox('Node:'#13#10'''' + attrNode.NodeName + ''''#13#10'Attribute:'#13#10'''' + Attr.NodeName + ''''#13#10'Value:'#13#10'''' + Attr.NodeValue + ''''#13#10'To find:'#13#10'''' + AttrValue + '''', mbInformation, MB_OK);
          if (Attr.NodeName = attrName) then
          begin
            if (Attr.NodeValue = attrValue) then
            begin
              //MsgBox('Attribute found.', mbInformation, MB_OK);
              resultnode:=attrNode;
              Result:=true;
              Exit;
            end
            else
            begin
              Result:=false;
              Exit;
            end;
          end;
        end;
      end;
    end;
  end;
end;

// Use of SeekNode: Remove node
function removeNode(const ParentNode: Variant; subNodePath, attrName, attrValue :String): Boolean;
var
  resultNode: Variant;
begin
  Result:=SeekNode(ParentNode, resultNode, subNodePath, attrName, attrValue, true);
  if (Result) then
    ParentNode.removeChild(resultNode);
end;

// Use of SeekNode: test node existence
function hasNode(const ParentNode: Variant; subNodePath, attrName, attrValue :String): Boolean;
var
  resultNode: Variant;
begin
  Result:=SeekNode(ParentNode, resultNode, subNodePath, attrName, attrValue, true);
end;

// Adds a single assembly binding block into the xml
procedure AddAssemblyBinding(const XmlDoc: Variant; const ParentNode: Variant; aiName, aiCulture, aiKey, brOld, brNew, cbVer, cbHref: String);
var
  dependentAssemblyNode: Variant;
  assemblyIdentityNode: Variant;
  bindingRedirectNode: Variant;
  codeBaseNode: Variant;
  publisherPolicyNode: Variant;
begin
//        <assemblyIdentity name="ECompas.Runtime" culture="" publicKeyToken="f27ad8cb97726f87" />
//        <bindingRedirect oldVersion="3.0.1.0 - 3.0.1.133" newVersion="3.0.1.133" />
//        <codeBase version="3.0.1.133" href="[TARGETDIR]Ecompas.Runtime.dll" />
//        <publisherPolicy apply="no"/>

  dependentAssemblyNode:= XMLDoc.createElement('dependentAssembly');

  assemblyIdentityNode:= XMLDoc.createElement('assemblyIdentity');
  assemblyIdentityNode.setAttribute('name', aiName);
  assemblyIdentityNode.setAttribute('culture', aiCulture);
  assemblyIdentityNode.setAttribute('publicKeyToken', aiKey);
  dependentAssemblyNode.appendChild(assemblyIdentityNode);

  if ((brOld <> '') and (brNew <> '')) then
  begin
    bindingRedirectNode:= XMLDoc.createElement('bindingRedirect');
    bindingRedirectNode.setAttribute('oldVersion', brOld);
    bindingRedirectNode.setAttribute('newVersion', brNew);
    dependentAssemblyNode.appendChild(bindingRedirectNode);
  end;

  codeBaseNode:= XMLDoc.createElement('codeBase');
  codeBaseNode.setAttribute('version', cbVer);
  codeBaseNode.setAttribute('href', cbHref);
  dependentAssemblyNode.appendChild(codeBaseNode);

  publisherPolicyNode:= XMLDoc.createElement('publisherPolicy');
  publisherPolicyNode.setAttribute('apply', 'no');
  dependentAssemblyNode.appendChild(publisherPolicyNode);

  // Doesn't work? No idea why it gives it an xmlns while its parent already has one.
  //dependentAssemblyNode.RemoveAttribute('xmlns');

  // It seems the actual variables of the nodes are somehow lost after adding
  // them to a parent - so add everything to them in advance!
  ParentNode.appendChild(dependentAssemblyNode);
end;

function UpdateConfig(const AFileName, Appdir: string; delete:Boolean): boolean;
var
  XMLDoc: Variant;
  RootNode, MainNode, AddNode: Variant;
  bECompasRuntime, bECompasMetamodel, bECompasDatabaseMS:  Boolean;

begin
  try
    XMLDoc := CreateOleObject('MSXML2.DOMDocument');
  except
    RaiseException('MSXML is required to complete the post-installation process.'#13#10#13#10'(Error ''' + GetExceptionMessage + ''' occurred)');
  end;  

  XMLDoc.async := False;
  XMLDoc.resolveExternals := False;
  XMLDoc.load(AFileName);

  if XMLDoc.parseError.errorCode <> 0 then
  begin
    MsgBox('XML processing error:'#13#10 + XMLDoc.parseError.reason, mbInformation, MB_OK);
    Result:=False;
    exit;
  end;
  XMLDoc.setProperty('SelectionLanguage', 'XPath');

  RootNode:=XMLDoc.documentElement;
  if (RootNode.nodeName <> 'configuration') then
  begin
    MsgBox('XML processing error:'#13#10'Root element ''configuration'' not found.', mbInformation, MB_OK);
    Result:=False;
    exit;
  end;
  MainNode:=EnsureXPath(XMLDoc, 'runtime/assemblyBinding');

  bECompasRuntime := HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Runtime');
  bECompasMetamodel := HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Metamodel');
  bECompasDatabaseMS := HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Database.MS');

  if (not delete) then
  begin

    if not bECompasRuntime then
      AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Runtime', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Runtime.dll');
    if not bECompasMetamodel then
      AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Metamodel', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Metamodel.dll');
    if not bECompasDatabaseMS then
      AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Database.MS', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Database.MS.dll');
  end
  else
  begin
    removeNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Runtime');
    removeNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Metamodel');
    removeNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Database.MS');
  end;

  MainNode:=EnsureXPath(XMLDoc, 'appSettings');
  if (not delete) then
  begin
    //<add key="logdir" value=".\log" />
    if (not HasNode(MainNode,'add','key','logdir')) then
    begin
      AddNode:= XMLDoc.createElement('add');
      AddNode.setAttribute('key', 'logdir');
      AddNode.setAttribute('value', '.\log');
      MainNode.appendChild(AddNode);
    end;
  end
  else
  begin
    removeNode(MainNode,'add','key','logdir');
  end;

  XMLDoc.Save(AFileName); 
  Result:=true;
end;

Originalmente, elUpdateConfig La función se hizo así:

if (not HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Runtime')) then
  AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Runtime', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Runtime.dll');
if (not HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Metamodel')) then
  AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Metamodel', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Metamodel.dll');
if (not HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Database.MS')) then
  AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Database.MS', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Database.MS.dll');

Este código funcionó bien la primera vez, pero la segunda vez, dio lo anterior "Método desconocido"error ensetAttribute enAddAssemblyBinding. Se volvió más extraño ... cuando eliminé las tres líneas que configuraban los atributos para assemblyIdentityNode, el resto del código DID se ejecutaba bien para los otros nodos.

Lo único que puedo imaginar está relacionado es que estos son los nodos que pregunto en elHasNode Función para ver si el bloque ya existe. ¿DOM no puede manejar la consulta a través de cambios no guardados? Así que edité el código para hacer las comprobaciones de existencia por adelantado y almacenar el resultado en Boolean, porque pensé que tal vez el problema era buscar los nodos en un árbol modificado. Pero ahora da un error al tratar de anidar un nodo debajo de sí mismo o de su propio hijo ("msxml3.dll: no se permite insertar un nodo o su antecesor en sí mismo"), sobre eldependentAssemblyNode.appendChild(bindingRedirectNode); línea. Ninguno de estos errores tiene sentido alguno.

Me parece que me gusta mucho más.EnsureXPath, cuando se usó por segunda vez en una situación en la que tuvo que agregar nodos, también dio el error de anidación ilegal. Me da la sensación de que, de alguna manera, el objeto se vuelve nulo misteriosamente en algún lugar, y que ese nulo se ve como el nodo raíz en las funciones que manejan objetos de nodo.

¿Alguien tiene alguna idea de qué puede estar causando este comportamiento?

El XML que estoy editando normalmente se ve así:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
            <dependentAssembly>
                <assemblyIdentity name="AppString1" publicKeyToken="43265234666" culture="neutral"/>
                <bindingRedirect oldVersion="1.0.0.0-1.1.99.99" newVersion="1.2.0.0"/>
            </dependentAssembly>
            <dependentAssembly>
                <assemblyIdentity name="AppString2" publicKeyToken="43265234666" culture="neutral"/>
                <bindingRedirect oldVersion="1.0.0.0-1.1.99.99" newVersion="1.2.0.0"/>
            </dependentAssembly>
        </assemblyBinding>
    </runtime>
</configuration>

(con algunas más de estas secciones dependientes de ensamblaje ... pero eso no importa)

Respuestas a la pregunta(3)

Su respuesta a la pregunta