在inno Setup中用DOM添加节点到xml - 奇怪的问题

时间:2013-05-10 12:17:31

标签: xml dom inno-setup pascalscript

非常奇怪的问题:我使用DOM编辑一个xml文件(一个需要与我们交互的应用程序的.exe.config文件),但看到我必须批量添加几个类似的部分,我做了一个函数插入整个所需的块。

调用此功能一次完美无缺。之后使用不同的参数再次调用它会产生异常(请参阅代码下面的说明)。

代码:

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

最初,UpdateConfig功能是这样完成的:

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

此代码第一次运行良好,但第二次,它在setAttribute AddAssemblyBinding上给出了上述“未知方法”错误。它变得更奇怪......当我删除设置属性为assemblyIdentityNode的三行时,其余代码DID对其他节点运行良好。

我唯一可以想象的是,这些是我在HasNode函数中查询的节点,以查看该块是否已存在。 DOM不能通过未保存的更改来处理查询吗?所以我编辑了代码以提前进行存在检查并将结果存储在布尔值中,因为我认为问题可能是在修改树上寻找节点。但现在它在尝试将节点嵌套在自身或其自己的子节点上时出错(“msxml3.dll:不允许在其自身下插入节点或其祖先”){{1行。这些错误都没有任何意义。

我似乎更喜欢它。 dependentAssemblyNode.appendChild(bindingRedirectNode);,在必须添加节点的情况下第二次使用时,也会出现非法嵌套错误。我感觉到某种程度上,该对象在某处神秘地变为null,并且该null被视为处理节点对象的函数中的根节点。

有没有人知道可能导致这种行为的原因是什么?

我正在编辑的XML通常如下所示:

EnsureXPath

(还有更多这些dependentAssembly部分......但这几乎不重要)

3 个答案:

答案 0 :(得分:1)

最后,似乎没有办法摆脱这种混乱,我最终只是制作了一个外部应用来进行XML编辑。用于进行xml更改的工具只是提取到程序文件夹,并使用正确的参数在Run和UninstallRun部分中设置。

(需要使用UninstallRun部分,因为要编辑的XML是需要与我们的应用程序集成的外部应用程序的一部分。显然,如果您处于这种情况但需要在自己的程序中进行xml编辑,只需将应用程序解压缩到{tmp}并从那里运行一次应该足够了)

如果有人弄清楚是什么原因导致这个COM混乱失败,请添加另一个答案。然而,从我在创建外部应用程序时遇到的情况来看,它可能与XML树中的名称空间中的变化有关。

答案 1 :(得分:1)

不是解决方案,而是关于该主题的进一步信息。

概述:针对比Windows 7 / Server 2008 R2更新的任何内容的Unicode Inno安装程序应该没问题。对于那些平台和旧版本,我不会依赖在Inno Setup中使用MSXML。我根本没有测试ANSI Inno Setup(我听说经常会有更少的问题)。

详细信息:我也注意到使用MSXML Variant对象的类似问题。我看到的问题通常发生在使用Variants的方法关闭时(因此可能释放资源),并且我收到了DLL崩溃。在专门调用MSXML方法本身时从未出现过任何错误。所以这个问题可能是半PascalScript在处理COM对象时如何引用?我在Windows 10上开发并且没有测试我的安装程序的问题,但只有在部署之后我才开始注意到这种疯狂。这最终让我相信它与平台有关。

Platform                Version  Works  Notes
----------------------------------------------------------------
Windows 10              NT 10.0  YES
Windows Server 2016     NT 10.0  YES
Windows 8.1             NT 6.3   YES
Windows Server 2012 R2  NT 6.3   YES    Requires .NET 4.5.2
Windows 8               NT 6.2   ???    Most likely works
Windows Server 2012     NT 6.2   YES    Requires .NET 4.5.1
Windows 7               NT 6.1   NO
Windows Server 2008 R2  NT 6.1   NO
Windows Vista           NT 6.0   NO

请注意,基于NT 6.1或更早版本的任何内容都是问题开始发生的地方。我无法测试普通的Windows 8,但在这些假设下,我敢打赌它有效。我正在进行相当复杂的XML操作(删除节点,添加节点,修改节点,以及使用方便的XPath方法进行各种XPath查询),因此我相信您可能会看到相同的结果。我不知道为什么需要.NET(对于Server 2012测试),因为MSXML与.NET并没有任何关系,但是在安装这些框架时,可能会更新另一个第三方依赖项(这使得一切都很开心)。即使在应用Windows 7或Server 2008 R2的所有安全更新和修复程序后,问题仍然存在。对于所有其他平台,我在安装后没有执行任何其他更新(除非注意到.NET框架)。所有这些都安装该平台的最新Service Pack / Update。

任何特定版本说明

InnoSetup-5.5.9(u)

Windows 10 Version 1511 (Updated Feb 2016)
Windows 8.1 with Update
Windows 7 with SP1
Windows Vista with SP2

Windows Server 2016
Windows Server 2012 R2 with SP1
Windows Server 2012
Windows Server 2008 R2 with SP1

答案 2 :(得分:0)

我已经看到你的seekNode函数正在为它检查的第一个属性返回。我想你需要删除以下部分:

else begin Result:=false; Exit; end;

我不知道这是否能解决你的问题。