【问题标题】:XML Serialization leaves file blank after restartXML 序列化在重新启动后将文件留空
【发布时间】:2021-08-04 06:51:56
【问题描述】:

我们的工业设备软件的 .XML 设置文件变为空白,但它们的字节数仍然正确。

我感觉这可能是由客户关闭 PC 的方式引起的,因为这种情况往往发生在他们关闭、隔离和启动之后。我保存文件的方式是,

  1. 序列化为 %temp% 文件
  2. 验证新创建的文件是否以 开头
  3. 如果文件的 /backup 文件夹版本超过一天,请将现有文件复制到 /backup 文件夹
  4. 复制新文件以覆盖现有文件。

我认为这可能与编码、磁盘缓存、Windows 更新或 Windows 恢复有关。

寻找想法,因为我花了两年时间追查为什么会发生这种情况。

根据要求,这里是代码。

        public static bool SerializeObjXml(object Object2Serialize, string FilePath, Type type, bool gzip = false)
        {
            if (!Path.IsPathRooted(FilePath))
                FilePath = Path.Combine(ApplicationDir, FilePath);
            bool isSuccess = false;

            var tmpFile = Path.GetTempFileName();
            try
            {
                for (int i = 0; i < 3; i++)
                {
                    try
                    {
                        Directory.CreateDirectory(Path.GetDirectoryName(FilePath));
                        if (gzip)
                        {
                            using (var ms = new MemoryStream())
                            {
                                XmlSerializer bf = new XmlSerializer(type);
                                bf.Serialize(ms, Object2Serialize);

                                ms.Position = 0;
                                using (var fileStream = new BinaryWriter(File.Open(tmpFile, FileMode.Create)))
                                {
                                    using (GZipStream gzipStream = new GZipStream(fileStream.BaseStream, CompressionMode.Compress))
                                    {
                                        byte[] buffer = new byte[4096];
                                        int numRead;
                                        while ((numRead = ms.Read(buffer, 0, buffer.Length)) != 0)
                                        {
                                            gzipStream.Write(buffer, 0, numRead);
                                        }
                                    }
                                }
                            }
                            if (!FileChecker.isGZip(tmpFile))
                                throw new XmlException("Failed to write valid XML file " + FilePath);
                        }
                        else
                        {
                            using (var fs = new StreamWriter(File.Open(tmpFile, FileMode.Create), Encoding.UTF8))
                            {
                                XmlSerializer bf = new XmlSerializer(type);
                                bf.Serialize(fs, Object2Serialize);
                            }
                            if (!FileChecker.isXML(tmpFile))
                                throw new XmlException("Failed to write valid XML file " + FilePath);
                        }
                        isSuccess = true;
                        return true;
                    }
                    catch (XmlException)
                    {
                        return false;
                    }
                    catch (System.IO.DriveNotFoundException) { continue; }
                    catch (System.IO.DirectoryNotFoundException) { continue; }
                    catch (System.IO.FileNotFoundException) { continue; }
                    catch (System.IO.IOException) { continue; }
                }
            }
            finally
            {
                if (isSuccess)
                {
                    lock (FilePath)
                    {
                        try
                        {
                            //Delete existing .bak file
                            if (File.Exists(FilePath + ".bak"))
                            {
                                File.SetAttributes(FilePath + ".bak", FileAttributes.Normal);
                                File.Delete(FilePath + ".bak");
                            }
                        }
                        catch { }
                        try
                        {
                            //Make copy of file as .bak
                            if (File.Exists(FilePath))
                            {
                                File.SetAttributes(FilePath, FileAttributes.Normal);
                                File.Copy(FilePath, FilePath + ".bak", true);
                            }
                        }
                        catch { }
                        try
                        {
                            //Copy the temp file to the target
                            File.Copy(tmpFile, FilePath, true);
                            //Delete .bak file if no error
                            if (File.Exists(FilePath + ".bak"))
                                File.Delete(FilePath + ".bak");
                        }
                        catch { }
                    }
                }
                try
                {
                    //Delete the %temp% file
                    if (File.Exists(tmpFile))
                        File.Delete(tmpFile);
                }
                catch { }
            }
            return false;
        }

        public static class FileChecker
        {
            const string gzipSig = "1F-8B-08";
            static string xmlSig = "EF-BB-BF";// <?x";
            public static bool isGZip(string filepath)
            {
                return FileChecker.CheckSignature(filepath, (3, gzipSig)) != null;
            }
            public static bool isXML(string filepath)
            {
                return FileChecker.CheckSignature(filepath, (3, xmlSig)) != null;
            }
            public static bool isGZipOrXML(string filepath, out bool isGZip, out bool isXML)
            {
                var sig = FileChecker.CheckSignature(filepath, (3, gzipSig), (3, xmlSig));
                isXML = (sig == xmlSig);
                isGZip = (sig == gzipSig);
                return isXML || isGZip;
            }
            public static string CheckSignature(string filepath, params (int signatureSize, string expectedSignature)[] pairs)
            {
                if (String.IsNullOrEmpty(filepath))
                    throw new ArgumentException("Must specify a filepath");
                if (String.IsNullOrEmpty(pairs[0].expectedSignature))
                    throw new ArgumentException("Must specify a value for the expected file signature");
                int signatureSize = 0;
                foreach (var pair in pairs) 
                    if (pair.signatureSize > signatureSize)
                        signatureSize = pair.signatureSize;
                using (FileStream fs = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    if (fs.Length < signatureSize)
                        return null;
                    byte[] signature = new byte[signatureSize];
                    int bytesRequired = signatureSize;
                    int index = 0;
                    while (bytesRequired > 0)
                    {
                        int bytesRead = fs.Read(signature, index, bytesRequired);
                        bytesRequired -= bytesRead;
                        index += bytesRead;
                    }
                    foreach (var pair in pairs)
                    {
                        string actualSignature = BitConverter.ToString(signature, 0, pair.signatureSize);
                        if (actualSignature == pair.expectedSignature)
                            return actualSignature;
                    }
                }
                return null;
            }
        }

【问题讨论】:

  • 嗨。我知道为什么会发生这种情况。例如,您没有向磁盘上已分配字节的预先存在的文件写入任何内容,从而导致 N 字节的空文件,但是您需要发布源代码以供任何人做,而不是假设。您的问题不包含最小可复制示例。请查看此内容并修改您的问题,以便我们协助回答:stackoverflow.com/help/minimal-reproducible-example
  • 您是否正确关闭了文件以使其刷新?此外,磁盘缓存不会立即刷新到磁盘,因此不正确的关闭(例如,只是关闭系统)可能会保留分配的文件大小,但如果实际数据尚未刷新,则所有字节都为空。我已经看过很多次了。
  • 让客户用记事本打开xml文件,看看文件是否真的是空白。我认为 xml 文件可能有错误并且在程序中读取失败。在 Windows 中,我无法认为文件最终会变成空白。文件中必须有损坏的 xml。
  • @jdweng 我自己用记事本++检查了文件。它们实际上完全是 \0 空字符。
  • @MarkTolonen 是的,FileStream 被using(Stream stream){} 关闭,然后我打开一个新的 Filestream 来检查前 3 个字节并验证内容。

标签: c# .net windows serialization


【解决方案1】:

分配文件空间时的问题在关机期间不会发生写入,这会让您在分配了字节的文件而没有将数据刷新到磁盘的情况下。

在操作系统关闭期间,可能会引发 ThreadAbortException 触发您的 finally 块。

您可以尝试在返回语句之前调用Process.Start("shutdown", "-a") 进行复制,但在设置success = true 之后。

我建议简化您的代码,并在您的 try {} 语句中运行所有内容。这消除了在您尝试写入磁盘之前出现success = true 状态的可能性,然后在由 Windows 关闭触发的 finally 语句中触发该状态。


public static bool SerializeObjXml(
    object Object2Serialize, 
    string FilePath, 
    Type type, 
    bool gzip = false)
{

    if (!Path.IsPathRooted(FilePath))
        FilePath = Path.Combine(ApplicationDir, FilePath);
    Directory.CreateDirectory(FilePath);

    for (int i = 0; i < 3; i++)
    {
        try
        {
            var tempFi = SerializeToXmlFile(Object2Serialize, type, gzip);
            var fi = new FileInfo(FilePath);
            if (fi.Exists)
                fi.CopyTo(fi.FullName + ".bak", true);
            tempFi.CopyTo(fi.FullName, true);
            tempFi.Delete();
            return true;
        }
        catch (Exception ex)
        {
            string message = $"[{DateTime.Now}] Error serializing file {FilePath}. {ex}";
            File.WriteAllText(FilePath + ".log", message);
        }
    }
    return false;
}

附带说明,您可以简单地使用 [Stream.CopyTo][1] 并直接写入临时文件,无需中间流或手动缓冲区/字节读/写操作:

private static FileInfo SerializeToXmlFile(
    object Object2Serialize,
    Type type,
    bool gzip)
{
    var tmpFile = Path.GetTempFileName();
    var tempFi = new FileInfo(tmpFile);
    if (!gzip)
    {
        using (var fs = File.Open(tmpFile, FileMode.Create))
            (new XmlSerializer(type)).Serialize(fs, Object2Serialize);
        if (!FileChecker.isXML(tmpFile))
            throw new Exception($"Failed to write valid XML file: {tmpFile}");
    }
    else
    {
        using (var fs = File.Open(tmpFile, FileMode.CreateNew))
        using (var gz = new GZipStream(fs, CompressionMode.Compress))
            (new XmlSerializer(type)).Serialize(fs, Object2Serialize);

        if (!FileChecker.isGZip(tmpFile))
            throw new Exception($"Failed to write valid XML gz file: {tmpFile}");
    }
    return tempFi;
}

【讨论】:

    【解决方案2】:

    使用操作系统的移动或复制文件来覆盖现有文件是一个原子操作,这意味着它完全成功或不成功并且不与其他文件操作重叠。

    因此,如果这就是您实现第 4 步的方式,那么您应该工作。

    复制新文件以覆盖现有文件。

    如果您将现有文件清空并重新写入我怀疑可能是故障点的数据..

    【讨论】:

    • 我在上面的原始答案中添加了代码。我正在使用 File.Copy() 和覆盖 True 进行移动操作。
    • 可能是竞争条件 - 例如您有File.Exists() 检查,但在运行下一行之前,可能会在该行之后立即更改 - 文件操作的原子性仅适用于单个操作..
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-05-22
    • 1970-01-01
    • 2021-11-28
    • 1970-01-01
    • 1970-01-01
    • 2018-11-20
    • 2011-10-05
    相关资源
    最近更新 更多