简体   繁体   English

FileSystemWatcher - 文件是否可以使用

[英]FileSystemWatcher - is File ready to use

When a file is being copied to the file watcher folder, how can I identify whether the file is completely copied and ready to use?将文件复制到文件观察器文件夹时,如何确定文件是否已完全复制并可以使用? Because I am getting multiple events during file copy.因为我在文件复制期间收到多个事件。 (The file is copied via another program using File.Copy.) (该文件是通过另一个程序使用 File.Copy 复制的。)

When I ran into this problem, the best solution I came up with was to continually try to get an exclusive lock on the file;当我遇到这个问题时,我想出的最佳解决方案是不断尝试获取文件的排他锁; while the file is being written, the locking attempt will fail, essentially the method in this answer.在写入文件时,锁定尝试将失败,本质上是答案中的方法。 Once the file isn't being written to any more, the lock will succeed.一旦文件不再被写入,锁定就会成功。

Unfortunately, the only way to do that is to wrap a try/catch around opening the file, which makes me cringe - having to use try/catch is always painful.不幸的是,唯一的方法是在打开文件时使用 try/catch,这让我感到畏缩——不得不使用 try/catch 总是很痛苦。 There just doesn't seem to be any way around that, though, so it's what I ended up using.不过,似乎没有任何办法可以解决这个问题,所以这就是我最终使用的方法。

Modifying the code in that answer does the trick, so I ended up using something like this:修改该答案中的代码可以解决问题,因此我最终使用了以下内容:

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

I had this problem when writing a file.我在写文件时遇到了这个问题。 I got events before the file was fully written and closed.我在文件完全写入和关闭之前收到了事件。

The solution is to use a temporary filename and rename the file once finished.解决方案是使用临时文件名并在完成后重命名文件。 Then watch for the file rename event instead of file creation or change event.然后观察文件重命名事件而不是文件创建或更改事件。

Note: this problem is not solvable in generic case.注意:这个问题在一般情况下是无法解决的。 Without prior knowledge about file usage you can't know if other program(s) finished operation with the file.如果没有关于文件使用的先验知识,您就无法知道其他程序是否完成了对文件的操作。

In your particular case you should be able to figure out what operations File.Copy consist of.在您的特定情况下,您应该能够弄清楚 File.Copy 包含哪些操作。

Most likely destination file is locked during whole operation.最有可能的目标文件在整个操作过程中被锁定。 In this case you should be able to simply try to open file and handle "sharing mode violation" exception.在这种情况下,您应该能够简单地尝试打开文件并处理“共享模式冲突”异常。

You can also wait for some time... - very unreliable option, but if you know size range of files you may be able to have reasonable delay to let Copy to finish.您也可以等待一段时间... - 非常不可靠的选项,但如果您知道文件的大小范围,您可能会有合理的延迟让复制完成。

You can also "invent" some sort of transaction system - ie create another file like "destination_file_name.COPYLOCK" which program that copies file would create before copying "destination_file_name" and delete afterward.您还可以“发明”某种交易系统 - 即创建另一个文件,如“destination_file_name.COPYLOCK”,该程序复制文件将在复制“destination_file_name”之前创建并随后删除。

Here is a method that will retry file access up to X number of times, with a Sleep between tries.这是一种最多可重试 X 次文件访问的方法,并在两次尝试之间进行Sleep If it never gets access, the application moves on:如果它永远无法访问,则应用程序继续:

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

It can be used like this:它可以像这样使用:

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

One possible solution (It worked in my case) is to use the Change event.一种可能的解决方案(在我的情况下有效)是使用 Change 事件。 You can log in the create event the name of the file just created and then catch the change event and verify if the file was just created.您可以将刚刚创建的文件的名称登录到创建事件中,然后捕获更改事件并验证文件是否刚刚创建。 When I manipulated the file in the change event it didn't throw me the error "File is in use"当我在更改事件中操作文件时,它没有向我抛出错误“文件正在使用中”

If you are doing some sort of inter-process communication, as I do, you may want to consider this solution:如果您像我一样进行某种进程间通信,您可能需要考虑以下解决方案:

  1. App A writes the file you are interested in, eg "Data.csv" App A 写入您感兴趣的文件,例如“Data.csv”
  2. When done, app A writes a 2nd file, eg.完成后,应用程序 A 写入第二个文件,例如。 "Data.confirmed" “数据确认”
  3. In your C# app make the FileWatcher listen to "*.confirmed" files.在您的 C# 应用程序中,让 FileWatcher 监听“*.confirmed”文件。 When you get this event you can safely read "Data.csv", as it is already completed by app A.当您收到此事件时,您可以安全地读取“Data.csv”,因为它已由应用程序 A 完成。
    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;

...of course, you can modify aspects of this to suit your needs. ...当然,您可以修改其中的各个方面以满足您的需要。

I have solved this issue with two features:我已经通过两个功能解决了这个问题:

  1. Implement the MemoryCache pattern seen in this question: A robust solution for FileSystemWatcher firing events multiple times实现在这个问题中看到的MemoryCache模式: 一个强大的 FileSystemWatcher 多次触发事件的解决方案
  2. Implement a try\\catch loop with a timeout for access实现带有访问超时的 try\\catch 循环

You need to collect average copy times in your environment and set the memory cache timeout to be at least as long as the shortest lock time on a new file.您需要收集环境中的平均复制时间,并将内存缓存超时设置为至少与新文件的最短锁定时间一样长。 This eliminates duplicates in your processing directive and allows some time for the copy to finish.这消除了处理指令中的重复项,并留出一些时间来完成复制。 You will have much better success on first try, which means less time spent in the try\\catch loop.您将在第一次尝试时获得更好的成功,这意味着在 try\\catch 循环中花费的时间更少。

Here is an example of the try\\catch loop:下面是 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;
}

Where TimeoutSeconds is a setting that you can put wherever you hold your settings.其中TimeoutSeconds是一个设置,您可以将其放在您保存设置的任何位置。 This can be tuned for your environment.这可以根据您的环境进行调整。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM