【问题标题】:FileSystemWatcher - is File ready to useFileSystemWatcher - 文件准备好使用了吗
【发布时间】:2012-08-29 09:12:30
【问题描述】:

当文件被复制到文件监视文件夹时,我如何确定文件是否已完全复制并准备好使用?因为我在文件复制期间收到了多个事件。 (该文件是通过另一个程序使用 File.Copy 复制的。)

【问题讨论】:

标签: c# .net filesystemwatcher


【解决方案1】:

当我遇到这个问题时,我想出的最佳解决方案是不断尝试获取文件的排他锁;在写入文件时,锁定尝试将失败,本质上是this 答案中的方法。一旦文件不再被写入,锁定就会成功。

不幸的是,这样做的唯一方法是在打开文件时使用 try/catch,这让我感到畏缩——不得不使用 try/catch 总是很痛苦。不过,似乎没有任何办法解决这个问题,所以这就是我最终使用的。

修改该答案中的代码就可以了,所以我最终使用了这样的东西:

private void WaitForFile(FileInfo file)
{
    FileStream stream = null;
    bool FileReady = false;
    while(!FileReady)
    {
        try
        {
            using(stream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None)) 
            { 
                FileReady = true; 
            }
        }
        catch (IOException)
        {
            //File isn't ready yet, so we need to keep on waiting until it is.
        }
        //We'll want to wait a bit between polls, if the file isn't ready.
        if(!FileReady) Thread.Sleep(1000);
    }
}

【讨论】:

  • 您可能想要添加计数并在尝试 x 次失败后退出尝试。
  • 这不会返回锁。因此它可能会报告 TRUE,然后您仍然会在访问时抛出错误,因为它在此方法结束和实际工作开始之间被锁定。最好遵循类似的模式,但使用您正在使用的实际代码。为什么要打开两次?要么让您的 WaitForOpen 返回流,要么将等待逻辑添加到您的操作中。
【解决方案2】:

这是一种重试文件访问最多 X 次的方法,尝试之间使用 Sleep。如果它永远无法访问,则应用程序继续:

private static bool GetIdleFile(string path)
{
    var fileIdle = false;
    const int MaximumAttemptsAllowed = 30;
    var attemptsMade = 0;

    while (!fileIdle && attemptsMade <= MaximumAttemptsAllowed)
    {
        try
        {
            using (File.Open(path, FileMode.Open, FileAccess.ReadWrite))
            {
                fileIdle = true;
            }
        }
        catch
        {
            attemptsMade++;
            Thread.Sleep(100);
        }
    }

    return fileIdle;
}

可以这样使用:

private void WatcherOnCreated(object sender, FileSystemEventArgs e)
{
    if (GetIdleFile(e.FullPath))
    {
        // Do something like...
        foreach (var line in File.ReadAllLines(e.FullPath))
        {
            // Do more...
        }
    }
}

【讨论】:

    【解决方案3】:

    我在写文件时遇到了这个问题。我在文件完全写入和关闭之前收到了事件。

    解决方案是使用临时文件名并在完成后重命名文件。然后注意文件重命名事件,而不是文件创建或更改事件。

    【讨论】:

    • 如果其他人将文件推送给您(例如通过 FTP 或类似的方式),这将不起作用 - 您必须知道文件已准备好使用,然后才能重命名并说它是准备使用:)
    【解决方案4】:

    注意:这个问题在一般情况下是无法解决的。如果没有关于文件使用的先验知识,您将无法知道其他程序是否完成了对该文件的操作。

    在您的特定情况下,您应该能够弄清楚 File.Copy 包含哪些操作。

    很可能目标文件在整个操作过程中被锁定。在这种情况下,您应该能够简单地尝试打开文件并处理“共享模式违规”异常。

    您也可以等待一段时间... - 非常不可靠的选项,但如果您知道文件的大小范围,您可能会有合理的延迟让 Copy 完成。

    您还可以“发明”某种事务系统 - 即创建另一个文件,例如“destination_file_name.COPYLOCK”,复制文件的程序将在复制“destination_file_name”之前创建并随后删除。

    【讨论】:

      【解决方案5】:
          private Stream ReadWhenAvailable(FileInfo finfo, TimeSpan? ts = null) => Task.Run(() =>
          {
              ts = ts == null ? new TimeSpan(long.MaxValue) : ts;
              var start = DateTime.Now;
              while (DateTime.Now - start < ts)
              {
                  Thread.Sleep(200);
                  try
                  {
                      return new FileStream(finfo.FullName, FileMode.Open);
                  }
                  catch { }
              }
              return null;
          })
          .Result;
      

      ...当然,您可以修改这方面的内容以满足您的需要。

      【讨论】:

      • ts 超时吗?如果是这样,您应该命名它,以便更清楚您在做什么。
      【解决方案6】:

      一种可能的解决方案(在我的情况下有效)是使用 Change 事件。您可以使用刚刚创建的文件的名称登录创建事件,然后捕获更改事件并验证文件是否刚刚创建。当我在更改事件中操作文件时,它没有向我抛出错误“文件正在使用中”

      【讨论】:

        【解决方案7】:

        如果您像我一样进行某种进程间通信,您可能需要考虑以下解决方案:

        1. App A 写入您感兴趣的文件,例如“Data.csv”
        2. 完成后,应用程序 A 会写入第二个文件,例如。 “数据.确认”
        3. 在您的 C# 应用程序中,让 FileWatcher 监听“*.confirmed”文件。当您收到此事件时,您可以安全地阅读“Data.csv”,因为它已由应用 A 完成。

        【讨论】:

        • 已经在existing answer中提及
        • @HereticMonkey 这与 Alexei Levenkov 的 proposal 不完全相同。思路类似,但实现方式不同。
        • @TheodorZoulias 我没有说这完全是什么。但想法是一样的;使用另一个文件作为事务机制。 Alexei 没有描述一个实现,只是一个想法的大纲,在这个答案中重复了。我没说是对是错。我认为将答案归功于这个人会很好,但那是我。
        • @HereticMonkey Alexei Levenkov 使用“.COPYLOCK”文件作为交易机制,而 Xcessity 使用“*.confirmed”文件作为通知机制。我认为 Xcessity 的方法更稳健,因为有人可以目视检查文件夹,并通过文件夹中存在旧的“.confirmed”文件而被警告出现问题。那是因为消费者负责删除“.confirmed”文件。 Alexei Levenkov 的方法是让生产者创建然后删除“.COPYLOCK”文件,以防消费者出现故障。
        • @TheodorZoulias 我对讨论“在写入观察的文件时创建另一个文件”这一核心思想的具体实现的相对优点并不感兴趣。老实说,你对我最初陈述的感受也没有。这是一条评论;在我看来,澄清答案的价值。就这样吧。
        【解决方案8】:

        我用两个功能解决了这个问题:

        1. 实现在这个问题中看到的MemoryCache 模式:A robust solution for FileSystemWatcher firing events multiple times
        2. 使用超时访问实现 try\catch 循环

        您需要收集环境中的平均复制时间,并将内存缓存超时设置为至少与新文件的最短锁定时间一样长。这消除了您的处理指令中的重复,并允许一些时间来完成复制。您在第一次尝试时会获得更好的成功,这意味着在 try\catch 循环中花费的时间更少。

        下面是 try\catch 循环的示例:

        public static IEnumerable<string> GetFileLines(string theFile)
        {
            DateTime startTime = DateTime.Now;
            TimeSpan timeOut = TimeSpan.FromSeconds(TimeoutSeconds);
            TimeSpan timePassed;
            do
            {
                try
                {
                    return File.ReadLines(theFile);
                }
                catch (FileNotFoundException ex)
                {
                    EventLog.WriteEntry(ProgramName, "File not found: " + theFile, EventLogEntryType.Warning, ex.HResult);
                    return null;
                }
                catch (PathTooLongException ex)
                {
                    EventLog.WriteEntry(ProgramName, "Path too long: " + theFile, EventLogEntryType.Warning, ex.HResult);
                    return null;
                }
                catch (DirectoryNotFoundException ex)
                {
                    EventLog.WriteEntry(ProgramName, "Directory not found: " + theFile, EventLogEntryType.Warning, ex.HResult);
                    return null;
                }
                catch (Exception ex)
                {
                    // We swallow all other exceptions here so we can try again
                    EventLog.WriteEntry(ProgramName, ex.Message, EventLogEntryType.Warning, ex.HResult);
                }
        
                Task.Delay(777).Wait();
                timePassed = DateTime.Now.Subtract(startTime);
            }
            while (timePassed < timeOut);
        
            EventLog.WriteEntry(ProgramName, "Timeout after waiting " + timePassed.ToString() + " seconds to read " + theFile, EventLogEntryType.Warning, 258);
            return null;
        }
        

        TimeoutSeconds 是一个设置,您可以将其放在您保存设置的任何位置。这可以根据您的环境进行调整。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-12-30
          • 2023-03-30
          • 1970-01-01
          • 1970-01-01
          • 2012-09-16
          • 2015-04-01
          • 2011-07-14
          • 2015-11-08
          相关资源
          最近更新 更多