FileSystemWatcher 无法访问文件,因为已在其他进程中使用

FileSystemWatcher cannot access files because used in other process

我正在编写一个 FileSystemWatcher,它会在图像上传到文件夹 A 时将图像从文件夹 A 复制到文件夹 B。我正在尝试将其用作服务器上的 windows 服务PC 但我遇到一些问题,我的文件在复制时被锁定。我想我已经找到问题的根源,但我没有运气解决它。因此,当我 运行 我的 windows 服务时,它总是在第一张或第二张图片上传时意外结束。我收到的错误消息是这样说的:The process cannot access the file 'filepath' because it is being used by another process.

我的代码的相关部分:

public void WatchForChanges()
{
    FileSystemWatcher watcher = new FileSystemWatcher();
    watcher.Path = Program.SourceFolder;
    watcher.Created += new FileSystemEventHandler(OnImageAdded);
    watcher.EnableRaisingEvents = true;
    watcher.IncludeSubdirectories = true;
}

public void OnImageAdded(object source, FileSystemEventArgs e)
{
    FileInfo file = new FileInfo(e.FullPath);
    ImageHandler handler = new ImageHandler();
    if (handler.IsImage(file))
    {
        handler.CopyImage(file);
    }
}

以及我的 CopyImage 方法,其中包括我针对此问题提出的解决方案之一,利用 while 循环捕获错误并重试图像复制:

public void CopyImage(FileSystemInfo file)
{
    // code that sets folder paths
    // code that sets folder paths

    bool retry = true;
    if (!Directory.Exists(targetFolderPath))
    {
        Directory.CreateDirectory(targetFolderPath);
    }
    while (retry)
    {
        try
        {
            File.Copy(file.FullName, targetPath, true);
            retry = false;
        }
        catch (Exception e)
        {
            Thread.Sleep(2000);
        }
    }
}

但是这个 CopyImage 解决方案只是不断复制同一个文件,这对我来说不是很理想。我希望这就够了,但遗憾的是我有一排图片在等着。

图像文件可能是由另一个应用程序创建的,该应用程序在读取和写入外部进程时使用独占访问锁(有关详细信息,请阅读 this,尤其是与 Microsoft [=25= 相关的段落) ]).您必须:

  • stop/kill正在使用文件的进程;
  • 等到文件不再被使用。

由于在您尝试使用应用程序复制文件时另一个进程可能正在写入文件,因此绝不推荐第一个选项。它也可能是一个反病毒检查新文件,即使在这种情况下第一个选项也不值得推荐。

您可以尝试将以下代码集成到您的 CopyImage 方法中,这样您的应用程序将等到该文件不再使用后再继续:

private Boolean WaitForFile(String filePath)
{
    Int32 tries = 0;

    while (true)
    {
        ++tries;

        Boolean wait = false;
        FileStream stream = null;

        try
        {
            stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None);
            break;
        }
        catch (Exception ex)
        {
            Logger.LogWarning("CopyImage({0}) failed to get an exclusive lock: {1}", filePath, ex.ToString());

            if (tries > 10)
            {
                Logger.LogWarning("CopyImage({0}) skipped the file after 10 tries.", filePath);
                return false;
            }

            wait = true;
        }
        finally
        {
            if (stream != null)
                stream.Close();
        }

        if (wait)
            Thread.Sleep(250);
    }

    Logger.LogWarning("CopyImage({0}) got an exclusive lock after {1} tries.", filePath, tries);

    return true;
}

虽然看起来很简单,但实际上复杂得不能令人满意。

问题是,当您收到通知时,正在写入文件的应用程序尚未完成处理...因此您遇到了并发问题。没有很好的方法可以知道文件何时关闭。好吧..一种方法是订阅日志事件——这就是 FileSystemWatcher 所做的——但这相当复杂并且需要 lot 的移动部件。走这条路线,您可以在文件关闭时收到通知。如果您有兴趣,请参阅 https://msdn.microsoft.com/en-us/library/windows/desktop/aa363798(v=vs.85).aspx

我会将工作分为两部分。我想我会启动一个 ThreadPool 线程来完成这项工作,并让它从 FileSystemWatcher's 事件处理程序写入的列表中读取它的工作。这样,事件处理程序returns很快。 ThreadPool 线程将遍历它的列表,尝试在文件上获得独占锁(类似于 Tommaso 的代码)。如果不能,它只是移动到下一个文件。每次成功复制时,它都会从列表中删除该文件。

您需要关注线程安全...因此您需要创建一个静态对象来协调对列表的写入。事件处理程序和 ThreadPool 线程都将在写入时持有锁。

这是整个方法的框架:

internal sealed class Copier: IDisposable
{
  static object sync = new object();
  bool quit;
  FileSystemWatcher watcher;
  List<string> work;

  internal Copier( string pathToWatch )
  {
    work = new List<string>();
    watcher = new FileSystemWatcher();
    watcher.Path = pathToWatch;
    watcher.Create += QueueWork;
    ThreadPool.QueueUserWorkItem( TryCopy );
  }

  void Dispose()
  {
    lock( sync ) quit = true;
  }

  void QueueWork( object source, FileSystemEventArgs args )
  {
    lock ( sync )
    {
      work.Add( args.FullPath );
    }
  }

  void TryCopy( object args )
  {
    List<string> localWork;
    while( true )
    {
      lock ( sync ) 
      { 
        if ( quit ) return;  //--> we've been disposed
        localWork = new List<string>( work );
      }
      foreach( var fileName in localWork )
      {
        var locked = true;
        try
        {
          using 
          ( var throwAway = new FileStream
            ( fileName, 
              FileMode.Open,
              FileAccess.Read,
              FileShare.None
            )
          ); //--> no-op  - will throw if we can't get exclusive read        
          locked = false;
        }
        catch { }
        if (!locked )
        {
          File.Copy( fileName, ... );
          lock( sync ) work.Remove( fileName );
        }
      }
    }
  }
}

未测试 - 将其写在答案中...但它或类似的内容将覆盖基础。