using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using SabreTools.Library.Data;
using SabreTools.Library.DatItems;
using SabreTools.Library.IO;
using SabreTools.Library.Tools;
namespace SabreTools.Library.DatFiles
{
///
/// Represents parsing and writing of a SofwareList, M1, or MAME XML DAT
///
internal class SoftwareList : DatFile
{
///
/// Constructor designed for casting a base DatFile
///
/// Parent DatFile to copy from
public SoftwareList(DatFile datFile)
: base(datFile)
{
}
///
/// Parse an SofwareList XML DAT and return all found games and roms within
///
/// Name of the file to be parsed
/// Index ID for the DAT
/// True if full pathnames are to be kept, false otherwise (default)
protected override void ParseFile(
// Standard Dat parsing
string filename,
int indexId,
// Miscellaneous
bool keep)
{
// Prepare all internal variables
XmlReader xtr = filename.GetXmlTextReader();
// If we got a null reader, just return
if (xtr == null)
return;
// Otherwise, read the file to the end
try
{
xtr.MoveToContent();
while (!xtr.EOF)
{
// We only want elements
if (xtr.NodeType != XmlNodeType.Element)
{
xtr.Read();
continue;
}
switch (xtr.Name)
{
case "softwarelist":
Header.Name = (Header.Name == null ? xtr.GetAttribute("name") ?? string.Empty : Header.Name);
Header.Description = (Header.Description == null ? xtr.GetAttribute("description") ?? string.Empty : Header.Description);
if (Header.ForceMerging == MergingFlag.None)
Header.ForceMerging = xtr.GetAttribute("forcemerging").AsMergingFlag();
if (Header.ForceNodump == NodumpFlag.None)
Header.ForceNodump = xtr.GetAttribute("forcenodump").AsNodumpFlag();
if (Header.ForcePacking == PackingFlag.None)
Header.ForcePacking = xtr.GetAttribute("forcepacking").AsPackingFlag();
xtr.Read();
break;
// We want to process the entire subtree of the machine
case "software":
ReadSoftware(xtr.ReadSubtree(), filename, indexId, keep);
// Skip the software now that we've processed it
xtr.Skip();
break;
default:
xtr.Read();
break;
}
}
}
catch (Exception ex)
{
Globals.Logger.Warning($"Exception found while parsing '{filename}': {ex}");
// For XML errors, just skip the affected node
xtr?.Read();
}
xtr.Dispose();
}
///
/// Read software information
///
/// XmlReader representing a software block
/// Name of the file to be parsed
/// Index ID for the DAT
/// True if full pathnames are to be kept, false otherwise (default)
private void ReadSoftware(
XmlReader reader,
// Standard Dat parsing
string filename,
int indexId,
// Miscellaneous
bool keep)
{
// If we have an empty software, skip it
if (reader == null)
return;
// Otherwise, add what is possible
reader.MoveToContent();
bool containsItems = false;
// Create a new machine
MachineType machineType = MachineType.NULL;
if (reader.GetAttribute("isbios").AsYesNo() == true)
machineType |= MachineType.Bios;
if (reader.GetAttribute("isdevice").AsYesNo() == true)
machineType |= MachineType.Device;
if (reader.GetAttribute("ismechanical").AsYesNo() == true)
machineType |= MachineType.Mechanical;
Machine machine = new Machine
{
Name = reader.GetAttribute("name"),
Description = reader.GetAttribute("name"),
Supported = reader.GetAttribute("supported").AsSupported(),
CloneOf = reader.GetAttribute("cloneof") ?? string.Empty,
Infos = new List(),
SharedFeatures = new List(),
DipSwitches = new List(),
MachineType = (machineType == MachineType.NULL ? MachineType.None : machineType),
};
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get the elements from the software
switch (reader.Name)
{
case "description":
machine.Description = reader.ReadElementContentAsString();
break;
case "year":
machine.Year = reader.ReadElementContentAsString();
break;
case "publisher":
machine.Publisher = reader.ReadElementContentAsString();
break;
case "category":
machine.Category = reader.ReadElementContentAsString();
break;
case "info":
var info = new ListXmlInfo();
info.Name = reader.GetAttribute("name");
info.Value = reader.GetAttribute("value");
machine.Infos.Add(info);
reader.Read();
break;
case "sharedfeat":
var sharedFeature = new SoftwareListSharedFeature();
sharedFeature.Name = reader.GetAttribute("name");
sharedFeature.Value = reader.GetAttribute("value");
machine.SharedFeatures.Add(sharedFeature);
reader.Read();
break;
case "part": // Contains all rom and disk information
containsItems = ReadPart(reader.ReadSubtree(), machine, filename, indexId, keep);
// Skip the part now that we've processed it
reader.Skip();
break;
default:
reader.Read();
break;
}
}
// If no items were found for this machine, add a Blank placeholder
if (!containsItems)
{
Blank blank = new Blank()
{
Source = new Source
{
Index = indexId,
Name = filename,
},
};
blank.CopyMachineInformation(machine);
// Now process and add the rom
ParseAddHelper(blank);
}
}
///
/// Read part information
///
/// XmlReader representing a part block
/// Machine information to pass to contained items
/// Name of the file to be parsed
/// Index ID for the DAT
/// True if full pathnames are to be kept, false otherwise (default)
private bool ReadPart(
XmlReader reader,
Machine machine,
// Standard Dat parsing
string filename,
int indexId,
// Miscellaneous
bool keep)
{
string areaname,
partname = string.Empty,
partinterface = string.Empty,
areaWidth,
areaEndinaness;
long? areasize = null;
var features = new List();
bool containsItems = false;
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "part")
{
partname = string.Empty;
partinterface = string.Empty;
features = new List();
}
if (reader.NodeType == XmlNodeType.EndElement && (reader.Name == "dataarea" || reader.Name == "diskarea"))
areasize = null;
reader.Read();
continue;
}
// Get the elements from the software
switch (reader.Name)
{
case "part":
partname = reader.GetAttribute("name");
partinterface = reader.GetAttribute("interface");
reader.Read();
break;
case "feature":
var feature = new SoftwareListFeature();
feature.Name = reader.GetAttribute("name");
feature.Value = reader.GetAttribute("value");
features.Add(feature);
reader.Read();
break;
case "dataarea":
areaname = reader.GetAttribute("name");
if (reader.GetAttribute("size") != null)
{
if (Int64.TryParse(reader.GetAttribute("size"), out long tempas))
areasize = tempas;
}
areaWidth = reader.GetAttribute("width");
areaEndinaness = reader.GetAttribute("endianness");
containsItems = ReadDataArea(
reader.ReadSubtree(),
machine,
partname,
partinterface,
features,
areaname,
areasize,
areaWidth,
areaEndinaness,
filename,
indexId,
keep);
// Skip the dataarea now that we've processed it
reader.Skip();
break;
case "diskarea":
areaname = reader.GetAttribute("name");
containsItems = ReadDiskArea(
reader.ReadSubtree(),
machine,
partname,
partinterface,
features,
areaname,
areasize,
filename,
indexId,
keep);
// Skip the diskarea now that we've processed it
reader.Skip();
break;
case "dipswitch":
// TODO: Use these dipswitches
var dipSwitch = new ListXmlDipSwitch();
dipSwitch.Name = reader.GetAttribute("name");
dipSwitch.Tag = reader.GetAttribute("tag");
dipSwitch.Mask = reader.GetAttribute("mask");
// Now read the internal tags
ReadDipSwitch(reader.ReadSubtree(), dipSwitch);
// Skip the dipswitch now that we've processed it
reader.Skip();
break;
default:
reader.Read();
break;
}
}
return containsItems;
}
///
/// Read dataarea information
///
/// XmlReader representing a dataarea block
/// Machine information to pass to contained items
/// Name of the containing part
/// Interface of the containing part
/// List of features from the parent part
/// Name of the containing area
/// Size of the containing area
/// Byte width of the containing area
/// Endianness of the containing area
/// Name of the file to be parsed
/// Index ID for the DAT
/// True if full pathnames are to be kept, false otherwise (default)
private bool ReadDataArea(
XmlReader reader,
Machine machine,
string partName,
string partInterface,
List features,
string areaName,
long? areaSize,
string areaWidth,
string areaEndianness,
// Standard Dat parsing
string filename,
int indexId,
// Miscellaneous
bool keep)
{
string key = string.Empty;
string temptype = reader.Name;
bool containsItems = false;
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get the elements from the software
switch (reader.Name)
{
case "rom":
containsItems = true;
// If the rom is continue or ignore, add the size to the previous rom
if (reader.GetAttribute("loadflag") == "continue" || reader.GetAttribute("loadflag") == "ignore")
{
int index = Items[key].Count - 1;
DatItem lastrom = Items[key][index];
if (lastrom.ItemType == ItemType.Rom)
{
((Rom)lastrom).Size += Sanitizer.CleanSize(reader.GetAttribute("size"));
}
Items[key].RemoveAt(index);
Items[key].Add(lastrom);
reader.Read();
continue;
}
DatItem rom = new Rom
{
Name = reader.GetAttribute("name"),
Size = Sanitizer.CleanSize(reader.GetAttribute("size")),
CRC = reader.GetAttribute("crc"),
MD5 = reader.GetAttribute("md5"),
#if NET_FRAMEWORK
RIPEMD160 = reader.GetAttribute("ripemd160"),
#endif
SHA1 = reader.GetAttribute("sha1"),
SHA256 = reader.GetAttribute("sha256"),
SHA384 = reader.GetAttribute("sha384"),
SHA512 = reader.GetAttribute("sha512"),
Offset = reader.GetAttribute("offset"),
ItemStatus = reader.GetAttribute("status").AsItemStatus(),
PartName = partName,
PartInterface = partInterface,
Features = features,
AreaName = areaName,
AreaSize = areaSize,
AreaWidth = areaWidth,
AreaEndianness = areaEndianness,
Value = reader.GetAttribute("value"),
LoadFlag = reader.GetAttribute("loadflag"),
Source = new Source
{
Index = indexId,
Name = filename,
},
};
rom.CopyMachineInformation(machine);
// Now process and add the rom
key = ParseAddHelper(rom);
reader.Read();
break;
default:
reader.Read();
break;
}
}
return containsItems;
}
///
/// Read diskarea information
///
/// XmlReader representing a diskarea block
/// Machine information to pass to contained items
/// Name of the containing part
/// Interface of the containing part
/// List of features from the parent part
/// Name of the containing area
/// Size of the containing area
/// Name of the file to be parsed
/// Index ID for the DAT
/// True if full pathnames are to be kept, false otherwise (default)
private bool ReadDiskArea(
XmlReader reader,
Machine machine,
string partname,
string partinterface,
List features,
string areaname,
long? areasize,
// Standard Dat parsing
string filename,
int indexId,
// Miscellaneous
bool keep)
{
string key = string.Empty;
string temptype = reader.Name;
bool containsItems = false;
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get the elements from the software
switch (reader.Name)
{
case "disk":
containsItems = true;
DatItem disk = new Disk
{
Name = reader.GetAttribute("name"),
MD5 = reader.GetAttribute("md5"),
#if NET_FRAMEWORK
RIPEMD160 = reader.GetAttribute("ripemd160"),
#endif
SHA1 = reader.GetAttribute("sha1"),
SHA256 = reader.GetAttribute("sha256"),
SHA384 = reader.GetAttribute("sha384"),
SHA512 = reader.GetAttribute("sha512"),
ItemStatus = reader.GetAttribute("status").AsItemStatus(),
Writable = reader.GetAttribute("writable").AsYesNo(),
PartName = partname,
PartInterface = partinterface,
Features = features,
AreaName = areaname,
AreaSize = areasize,
Source = new Source
{
Index = indexId,
Name = filename,
},
};
disk.CopyMachineInformation(machine);
// Now process and add the rom
key = ParseAddHelper(disk);
reader.Read();
break;
default:
reader.Read();
break;
}
}
return containsItems;
}
///
/// Read DipSwitch DipValues information
///
/// XmlReader representing a diskarea block
/// ListXMLDipSwitch to populate
private void ReadDipSwitch(XmlReader reader, ListXmlDipSwitch dipSwitch)
{
// If we have an empty dipswitch, skip it
if (reader == null)
return;
// Get list ready
dipSwitch.Values = new List();
// Otherwise, add what is possible
reader.MoveToContent();
while (!reader.EOF)
{
// We only want elements
if (reader.NodeType != XmlNodeType.Element)
{
reader.Read();
continue;
}
// Get the information from the dipswitch
switch (reader.Name)
{
case "dipvalue":
var dipValue = new ListXmlDipValue();
dipValue.Name = reader.GetAttribute("name");
dipValue.Value = reader.GetAttribute("value");
dipValue.Default = reader.GetAttribute("default").AsYesNo();
dipSwitch.Values.Add(dipValue);
reader.Read();
break;
default:
reader.Read();
break;
}
}
}
///
/// Create and open an output file for writing direct from a dictionary
///
/// Name of the file to write to
/// True if blank roms should be skipped on output, false otherwise (default)
/// True if the DAT was written correctly, false otherwise
public override bool WriteToFile(string outfile, bool ignoreblanks = false)
{
try
{
Globals.Logger.User($"Opening file for writing: {outfile}");
FileStream fs = FileExtensions.TryCreate(outfile);
// If we get back null for some reason, just log and return
if (fs == null)
{
Globals.Logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable");
return false;
}
XmlTextWriter xtw = new XmlTextWriter(fs, new UTF8Encoding(false))
{
Formatting = Formatting.Indented,
IndentChar = '\t',
Indentation = 1
};
// Write out the header
WriteHeader(xtw);
// Write out each of the machines and roms
string lastgame = null;
// Use a sorted list of games to output
foreach (string key in Items.SortedKeys)
{
List roms = Items[key];
// Resolve the names in the block
roms = DatItem.ResolveNames(roms);
for (int index = 0; index < roms.Count; index++)
{
DatItem rom = roms[index];
// There are apparently times when a null rom can skip by, skip them
if (rom.Name == null || rom.Machine.Name == null)
{
Globals.Logger.Warning("Null rom found!");
continue;
}
// If we have a different game and we're not at the start of the list, output the end of last item
if (lastgame != null && lastgame.ToLowerInvariant() != rom.Machine.Name.ToLowerInvariant())
WriteEndGame(xtw);
// If we have a new game, output the beginning of the new item
if (lastgame == null || lastgame.ToLowerInvariant() != rom.Machine.Name.ToLowerInvariant())
WriteStartGame(xtw, rom);
// If we have a "null" game (created by DATFromDir or something similar), log it to file
if (rom.ItemType == ItemType.Rom
&& ((Rom)rom).Size == -1
&& ((Rom)rom).CRC == "null")
{
Globals.Logger.Verbose($"Empty folder found: {rom.Machine.Name}");
lastgame = rom.Machine.Name;
continue;
}
// Now, output the rom data
WriteDatItem(xtw, rom, ignoreblanks);
// Set the new data to compare against
lastgame = rom.Machine.Name;
}
}
// Write the file footer out
WriteFooter(xtw);
Globals.Logger.Verbose("File written!" + Environment.NewLine);
xtw.Dispose();
fs.Dispose();
}
catch (Exception ex)
{
Globals.Logger.Error(ex.ToString());
return false;
}
return true;
}
///
/// Write out DAT header using the supplied StreamWriter
///
/// XmlTextWriter to output to
/// True if the data was written, false on error
private bool WriteHeader(XmlTextWriter xtw)
{
try
{
xtw.WriteStartDocument();
xtw.WriteDocType("softwarelist", null, "softwarelist.dtd", null);
xtw.WriteStartElement("softwarelist");
xtw.WriteRequiredAttributeString("name", Header.Name);
xtw.WriteRequiredAttributeString("description", Header.Description);
xtw.WriteOptionalAttributeString("forcepacking", Header.ForcePacking.FromPackingFlag(false));
xtw.WriteOptionalAttributeString("forcemerging", Header.ForceMerging.FromMergingFlag(false));
xtw.WriteOptionalAttributeString("forcenodump", Header.ForceNodump.FromNodumpFlag());
xtw.Flush();
}
catch (Exception ex)
{
Globals.Logger.Error(ex.ToString());
return false;
}
return true;
}
///
/// Write out Game start using the supplied StreamWriter
///
/// XmlTextWriter to output to
/// DatItem object to be output
/// True if the data was written, false on error
private bool WriteStartGame(XmlTextWriter xtw, DatItem datItem)
{
try
{
// No game should start with a path separator
datItem.Machine.Name = datItem.Machine.Name.TrimStart(Path.DirectorySeparatorChar);
// Build the state
xtw.WriteStartElement("software");
xtw.WriteRequiredAttributeString("name", datItem.Machine.Name);
if (!string.Equals(datItem.Machine.Name, datItem.Machine.CloneOf, StringComparison.OrdinalIgnoreCase))
xtw.WriteOptionalAttributeString("cloneof", datItem.Machine.CloneOf);
xtw.WriteOptionalAttributeString("supported", datItem.Machine.Supported.FromSupported());
xtw.WriteOptionalElementString("description", datItem.Machine.Description);
xtw.WriteOptionalElementString("year", datItem.Machine.Year);
xtw.WriteOptionalElementString("publisher", datItem.Machine.Publisher);
xtw.WriteOptionalElementString("category", datItem.Machine.Category);
if (datItem.Machine.Infos != null && datItem.Machine.Infos.Count > 0)
{
foreach (ListXmlInfo kvp in datItem.Machine.Infos)
{
xtw.WriteStartElement("info");
xtw.WriteRequiredAttributeString("name", kvp.Name);
xtw.WriteRequiredAttributeString("value", kvp.Value);
xtw.WriteEndElement();
}
}
if (datItem.Machine.SharedFeatures != null && datItem.Machine.SharedFeatures.Count > 0)
{
foreach (SoftwareListSharedFeature kvp in datItem.Machine.SharedFeatures)
{
xtw.WriteStartElement("sharedfeat");
xtw.WriteRequiredAttributeString("name", kvp.Name);
xtw.WriteRequiredAttributeString("value", kvp.Value);
xtw.WriteEndElement();
}
}
if (datItem.Machine.DipSwitches != null && datItem.Machine.DipSwitches.Count > 0)
{
foreach (ListXmlDipSwitch dip in datItem.Machine.DipSwitches)
{
xtw.WriteStartElement("dipswitch");
xtw.WriteRequiredAttributeString("name", dip.Name);
xtw.WriteRequiredAttributeString("tag", dip.Tag);
xtw.WriteRequiredAttributeString("mask", dip.Mask);
foreach (ListXmlDipValue dipval in dip.Values)
{
xtw.WriteStartElement("dipvalue");
xtw.WriteRequiredAttributeString("name", dipval.Name);
xtw.WriteRequiredAttributeString("value", dipval.Value);
xtw.WriteRequiredAttributeString("default", dipval.Default == true ? "yes" : "no");
xtw.WriteEndElement();
}
// End dipswitch
xtw.WriteEndElement();
}
}
xtw.Flush();
}
catch (Exception ex)
{
Globals.Logger.Error(ex.ToString());
return false;
}
return true;
}
///
/// Write out Game start using the supplied StreamWriter
///
/// XmlTextWriter to output to
/// True if the data was written, false on error
private bool WriteEndGame(XmlTextWriter xtw)
{
try
{
// End software
xtw.WriteEndElement();
xtw.Flush();
}
catch (Exception ex)
{
Globals.Logger.Error(ex.ToString());
return false;
}
return true;
}
///
/// Write out DatItem using the supplied StreamWriter
///
/// XmlTextWriter to output to
/// DatItem object to be output
/// True if blank roms should be skipped on output, false otherwise (default)
/// True if the data was written, false on error
private bool WriteDatItem(XmlTextWriter xtw, DatItem datItem, bool ignoreblanks = false)
{
// If we are in ignore blanks mode AND we have a blank (0-size) rom, skip
if (ignoreblanks && (datItem.ItemType == ItemType.Rom && ((datItem as Rom).Size == 0 || (datItem as Rom).Size == -1)))
return true;
try
{
// Pre-process the item name
ProcessItemName(datItem, true);
// Build the state
xtw.WriteStartElement("part");
xtw.WriteRequiredAttributeString("name", datItem.PartName);
xtw.WriteRequiredAttributeString("interface", datItem.PartInterface);
if (datItem.Features != null && datItem.Features.Count > 0)
{
foreach (SoftwareListFeature kvp in datItem.Features)
{
xtw.WriteStartElement("feature");
xtw.WriteRequiredAttributeString("name", kvp.Name);
xtw.WriteRequiredAttributeString("value", kvp.Value);
xtw.WriteEndElement();
}
}
string areaName = datItem.AreaName;
switch (datItem.ItemType)
{
case ItemType.Disk:
var disk = datItem as Disk;
if (string.IsNullOrWhiteSpace(areaName))
areaName = "cdrom";
xtw.WriteStartElement("diskarea");
xtw.WriteRequiredAttributeString("name", areaName);
xtw.WriteOptionalAttributeString("size", disk.AreaSize.ToString());
xtw.WriteStartElement("disk");
xtw.WriteRequiredAttributeString("name", disk.Name);
xtw.WriteOptionalAttributeString("md5", disk.MD5?.ToLowerInvariant());
#if NET_FRAMEWORK
xtw.WriteOptionalAttributeString("ripemd160", disk.RIPEMD160?.ToLowerInvariant());
#endif
xtw.WriteOptionalAttributeString("sha1", disk.SHA1?.ToLowerInvariant());
xtw.WriteOptionalAttributeString("sha256", disk.SHA256?.ToLowerInvariant());
xtw.WriteOptionalAttributeString("sha384", disk.SHA384?.ToLowerInvariant());
xtw.WriteOptionalAttributeString("sha512", disk.SHA512?.ToLowerInvariant());
xtw.WriteOptionalAttributeString("status", disk.ItemStatus.FromItemStatus(false));
xtw.WriteOptionalAttributeString("writable", disk.Writable.FromYesNo());
xtw.WriteEndElement();
// End diskarea
xtw.WriteEndElement();
break;
case ItemType.Rom:
var rom = datItem as Rom;
if (string.IsNullOrWhiteSpace(areaName))
areaName = "rom";
xtw.WriteStartElement("dataarea");
xtw.WriteRequiredAttributeString("name", areaName);
xtw.WriteOptionalAttributeString("size", rom.AreaSize.ToString());
xtw.WriteOptionalAttributeString("width", rom.AreaWidth);
xtw.WriteOptionalAttributeString("endianness", rom.AreaEndianness);
xtw.WriteStartElement("rom");
xtw.WriteRequiredAttributeString("name", rom.Name);
if (rom.Size != -1) xtw.WriteAttributeString("size", rom.Size.ToString());
xtw.WriteOptionalAttributeString("crc", rom.CRC?.ToLowerInvariant());
xtw.WriteOptionalAttributeString("md5", rom.MD5?.ToLowerInvariant());
#if NET_FRAMEWORK
xtw.WriteOptionalAttributeString("ripemd160", rom.RIPEMD160?.ToLowerInvariant());
#endif
xtw.WriteOptionalAttributeString("sha1", rom.SHA1?.ToLowerInvariant());
xtw.WriteOptionalAttributeString("sha256", rom.SHA256?.ToLowerInvariant());
xtw.WriteOptionalAttributeString("sha384", rom.SHA384?.ToLowerInvariant());
xtw.WriteOptionalAttributeString("sha512", rom.SHA512?.ToLowerInvariant());
xtw.WriteOptionalAttributeString("offset", rom.Offset);
xtw.WriteOptionalAttributeString("value", rom.Value);
xtw.WriteOptionalAttributeString("status", rom.ItemStatus.FromItemStatus(false));
xtw.WriteOptionalAttributeString("loadflag", rom.LoadFlag);
xtw.WriteEndElement();
// End dataarea
xtw.WriteEndElement();
break;
}
// End part
xtw.WriteEndElement();
xtw.Flush();
}
catch (Exception ex)
{
Globals.Logger.Error(ex.ToString());
return false;
}
return true;
}
///
/// Write out DAT footer using the supplied StreamWriter
///
/// XmlTextWriter to output to
/// True if the data was written, false on error
private bool WriteFooter(XmlTextWriter xtw)
{
try
{
// End software
xtw.WriteEndElement();
// End softwarelist
xtw.WriteEndElement();
xtw.Flush();
}
catch (Exception ex)
{
Globals.Logger.Error(ex.ToString());
return false;
}
return true;
}
}
}