Dodawanie węzłów do xml z DOM w inno Setup - dziwne problemy

Bardzo dziwny problem: używam DOM do edycji pliku xml (plik .exe.config dla aplikacji, która wymaga interakcji z naszą), ale ponieważ muszę dodawać zbiorczo kilka podobnych sekcji, stworzyłem funkcję do wstawienia cały potrzebny blok.

Wywołanie tej funkcji działa idealnie. Wywołanie go ponownie z różnymi parametrami zaraz potem daje wyjątek (patrz wyjaśnienie poniżej kodu).

Kod:

// 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;

PierwotnieUpdateConfig funkcja została wykonana w ten sposób:

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');

Ten kod działał dobrze za pierwszym razem, ale drugi raz dał wspomniane wcześniej „Nieznana metoda„błąd włączonysetAttribute wAddAssemblyBinding. Stało się to bardziej dziwne ... kiedy usunąłem trzy linie ustawiające atrybuty do AssemblyIdentityNode, reszta kodu DID działała dobrze dla innych węzłów.

Jedyną rzeczą, jaką mogłem sobie wyobrazić, jest to, że są to węzły, które sprawdzam wHasNode funkcja, aby sprawdzić, czy blok już istnieje. Czy DOM nie obsługuje zapytań poprzez niezapisane zmiany? Zmontowałem więc kod, aby istnienie sprawdziło się wcześniej i zapisałem wynik w Booleans, ponieważ pomyślałem, że może problem polegał na poszukiwaniu węzłów na zmodyfikowanym drzewie. Ale teraz daje błąd związany z próbą zagnieżdżenia węzła pod sobą lub własnym dzieckiem („msxml3.dll: Wstawianie węzła lub jego przodka nie jest dozwolone”), nadependentAssemblyNode.appendChild(bindingRedirectNode); linia. Żaden z tych błędów nie ma żadnego sensu.

Wydaje mi się, że ładuję się bardziej.EnsureXPath, użyty po raz drugi w sytuacji, w której musiał dodać węzły, podał również nielegalny błąd zagnieżdżania. Mam wrażenie, że jakoś obiekt w tajemniczy sposób staje się gdzieś null i że ten null jest postrzegany jako węzeł główny w funkcjach obsługujących obiekty węzłowe.

Czy ktoś ma jakąkolwiek wskazówkę, co może powodować to zachowanie?

XML, który edytuję, wygląda zazwyczaj tak:

<?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>

(z kilkoma tymi sekcjami zależnymi od montażu ... ale to się nie liczy)

questionAnswers(3)

yourAnswerToTheQuestion