【问题标题】:Wait for file to be freed by process等待文件被进程释放
【发布时间】:2010-11-27 06:25:29
【问题描述】:

如何等待文件空闲,以便ss.Save() 可以用新文件覆盖它?如果我一起运行两次(ish),我会收到 generic GDI+ 错误。

///<summary>
/// Grabs a screen shot of the App and saves it to the C drive in jpg
///</summary>
private static String GetDesktopImage(DevExpress.XtraEditors.XtraForm whichForm)
{
    Rectangle bounds = whichForm.Bounds;

    // This solves my problem but creates a clutter issue
    // var timeStamp = DateTime.Now.ToString("ddd-MMM-dd-yyyy-hh-mm-ss");
    // var fileName = "C:\\HelpMe" + timeStamp + ".jpg";

    var fileName = "C:\\HelpMe.jpg";
    File.Create(fileName);
    using (Bitmap ss = new Bitmap(bounds.Width, bounds.Height))
    using (Graphics g = Graphics.FromImage(ss))
    {
        g.CopyFromScreen(whichForm.Location, Point.Empty, bounds.Size);
        ss.Save(fileName, ImageFormat.Jpeg);
    }

    return fileName;
}

【问题讨论】:

标签: c# .net winforms file-io ioexception


【解决方案1】:

没有功能可以让您等待特定句柄/文件系统位置可用于写入。可悲的是,您所能做的就是轮询句柄以进行写作。

【讨论】:

    【解决方案2】:
    bool isLocked = true;
    while (isLocked)
     try {
      System.IO.File.Move(filename, filename2);
      isLocked = false;
     }
     catch { }
     System.IO.File.Move(filename2, filename);
    

    【讨论】:

    • 移动文件以查明它是否被锁定,无论文件用途的上下文知识如何,都不是一个好方法。
    • 你可能不想这样做^。一些不同的进程可能会检查文件是否同时存在并创建另一个文件或其他文件。
    【解决方案3】:

    这样的函数可以做到:

    public static bool IsFileReady(string filename)
    {
        // If the file can be opened for exclusive access it means that the file
        // is no longer locked by another process.
        try
        {
            using (FileStream inputStream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.None))
                return inputStream.Length > 0;
        }
        catch (Exception)
        {
            return false;
        }
    }
    

    将其粘贴在while 循环中,您就会有一些东西会阻塞,直到文件可以访问:

    public static void WaitForFile(string filename)
    {
        //This will lock the execution until the file is ready
        //TODO: Add some logic to make it async and cancelable
        while (!IsFileReady(filename)) { }
    }
    

    【讨论】:

    • 捕获所有异常是一种非常糟糕的做法,您应该更准确地确定文件根本无法访问的事实。
    • 不应该在某个地方有一个 sleep() - 否则应用程序可能会变得无响应
    • 另外一个问题是您在返回响应和打开文件的另一段代码之间进入竞争条件,另一个系统可以进入并锁定文件。
    【解决方案4】:

    您可以让系统等待,直到进程关闭。

    就这么简单:

    Process.Start("the path of your text file or exe").WaitForExit();

    【讨论】:

    • 谁应该关闭(退出)新启动的进程?
    【解决方案5】:

    如果您在写入文件之前检查访问权限,则其他进程可能会在您设法进行写入之前再次抢夺访问权限。因此,我会建议以下两个之一:

    1. 将您要执行的操作封装在不会隐藏任何其他错误的重试范围内
    2. 创建一个包装器方法,该方法会等待您获取流并使用该流

    获取信息流

    private FileStream GetWriteStream(string path, int timeoutMs)
    {
        var time = Stopwatch.StartNew();
        while (time.ElapsedMilliseconds < timeoutMs)
        {
            try
            {
                return new FileStream(path, FileMode.Create, FileAccess.Write);
            }
            catch (IOException e)
            {
                // access error
                if (e.HResult != -2147024864)
                    throw;
            }
        }
    
        throw new TimeoutException($"Failed to get a write handle to {path} within {timeoutMs}ms.");
    }
    

    然后像这样使用它:

    using (var stream = GetWriteStream("path"))
    {
        using (var writer = new StreamWriter(stream))
            writer.Write("test");
    }
    

    重试范围

    private void WithRetry(Action action, int timeoutMs = 1000)
    {
        var time = Stopwatch.StartNew();
        while(time.ElapsedMilliseconds < timeoutMs)
        {
            try
            {
                action();
                return;
            }
            catch (IOException e)
            {
                // access error
                if (e.HResult != -2147024864)
                    throw;
            }
        }
        throw new Exception("Failed perform action within allotted time.");
    }
    

    然后使用WithRetry(() => File.WriteAllText(Path.Combine(_directory, name), contents));

    【讨论】:

    • 我还为包含此行为的类创建了一个要点。当然请记住,如果多个类以冲突的方式读写同一个文件,这样做可能意味着您的体系结构存在问题。您最终可能会以这种方式丢失数据。 gist.github.com/ddikman/667f309706fdf4f68b9fab2827b1bcca
    • 我不知道为什么这不是公认的答案。代码更安全;正如 Gordon Thompson 的回答所暗示的那样,在 while 循环中调用 IsFileReady 可能会失败。另一个进程可能会在循环条件检查文件是否可用和您的进程尝试实际访问它之间锁定文件。只有 e.HResult 无法访问,因为它是 protected
    • 感谢您的支持,尽管相比之下我建议的解决方案相当混乱。但是,我不太喜欢它的外观,因为框架中没有内置支持,因此您几乎没有选择。虽然我使用的是 HResult,但框架版本之间可能会有所不同,我确信还有一些其他属性可用于检测 IOException 包含的错误。
    • 我知道你可以使用 Message 属性并进行字符串比较,但这有点丑 IMO
    • 我同意,除非必须,否则我不会查看消息。不过,我仔细检查了 MSDN 关于 HResult 的信息,并且在框架的更高版本(3.5 之后)中它是公开的。 msdn.microsoft.com/en-us/library/…
    【解决方案6】:

    这里有一个解决方案,对某些用户来说可能是多余的。我创建了一个新的静态类,它有一个仅在文件完成复制时触发的事件。

    用户通过调用FileAccessWatcher.RegisterWaitForFileAccess(filePath)注册他们想要观看的文件。如果该文件尚未被监视,则会启动一个新任务,该任务会反复检查该文件以查看它是否可以打开。每次它检查它也会读取文件大小。如果文件大小在预定义的时间(在我的示例中为 5 分钟)内没有增加,则退出循环。

    当循环退出可访问文件或超时时,将触发FileFinishedCopying 事件。

    public class FileAccessWatcher
    {
        // this list keeps track of files being watched
        private static ConcurrentDictionary<string, FileAccessWatcher> watchedFiles = new ConcurrentDictionary<string, FileAccessWatcher>();
    
        public static void RegisterWaitForFileAccess(string filePath)
        {
            // if the file is already being watched, don't do anything
            if (watchedFiles.ContainsKey(filePath))
            {
                return;
            }
            // otherwise, start watching it
            FileAccessWatcher accessWatcher = new FileAccessWatcher(filePath);
            watchedFiles[filePath] = accessWatcher;
            accessWatcher.StartWatching();
        }
    
        /// <summary>
        /// Event triggered when the file is finished copying or when the file size has not increased in the last 5 minutes.
        /// </summary>
        public static event FileSystemEventHandler FileFinishedCopying;
    
        private static readonly TimeSpan MaximumIdleTime = TimeSpan.FromMinutes(5);
    
        private readonly FileInfo file;
    
        private long lastFileSize = 0;
    
        private DateTime timeOfLastFileSizeIncrease = DateTime.Now;
    
        private FileAccessWatcher(string filePath)
        {
            this.file = new FileInfo(filePath);
        }
    
        private Task StartWatching()
        {
            return Task.Factory.StartNew(this.RunLoop);
        }
    
        private void RunLoop()
        {
            while (this.IsFileLocked())
            {
                long currentFileSize = this.GetFileSize();
                if (currentFileSize > this.lastFileSize)
                {
                    this.lastFileSize = currentFileSize;
                    this.timeOfLastFileSizeIncrease = DateTime.Now;
                }
    
                // if the file size has not increased for a pre-defined time limit, cancel
                if (DateTime.Now - this.timeOfLastFileSizeIncrease > MaximumIdleTime)
                {
                    break;
                }
            }
    
            this.RemoveFromWatchedFiles();
            this.RaiseFileFinishedCopyingEvent();
        }
    
        private void RemoveFromWatchedFiles()
        {
            FileAccessWatcher accessWatcher;
            watchedFiles.TryRemove(this.file.FullName, out accessWatcher);
        }
    
        private void RaiseFileFinishedCopyingEvent()
        {
            FileFinishedCopying?.Invoke(this,
                new FileSystemEventArgs(WatcherChangeTypes.Changed, this.file.FullName, this.file.Name));
        }
    
        private long GetFileSize()
        {
            return this.file.Length;
        }
    
        private bool IsFileLocked()
        {
            try
            {
                using (this.file.Open(FileMode.Open)) { }
            }
            catch (IOException e)
            {
                var errorCode = Marshal.GetHRForException(e) & ((1 << 16) - 1);
    
                return errorCode == 32 || errorCode == 33;
            }
    
            return false;
        }
    }
    

    示例用法:

    // register the event
    FileAccessWatcher.FileFinishedCopying += FileAccessWatcher_FileFinishedCopying;
    
    // start monitoring the file (put this inside the OnChanged event handler of the FileSystemWatcher
    FileAccessWatcher.RegisterWaitForFileAccess(fileSystemEventArgs.FullPath);
    

    处理 FileFinishedCopyingEvent:

    private void FileAccessWatcher_FileFinishedCopying(object sender, FileSystemEventArgs e)
    {
        Console.WriteLine("File finished copying: " + e.FullPath);
    }
    

    【讨论】:

    • 这是一个很好的解决方案。在我看来,这是唯一的防弹解决方案。其他解决方案不适用于所有情况。例如,此解决方案适用于:打开 2 个 windows 文件夹,将文件从目录 a 复制到监视目录。该解决方案正确地为我提供了 FileFinishedCopying 上的单个事件调用。在这种情况下,其他解决方案(例如打开文件/读取文件)仍然会产生多次点击。感谢您提供此解决方案!
    • 从我的初步测试来看,这似乎运行良好,但类实例需要使用创建实例时指定的原始文件路径而不是“this.file”从 watchFiles 字典中删除自身。全名'。其他潜在的增强功能是在 RunLoop 的“while”循环中包含一个短暂的延迟,以确保其他进程可以使用 CPU,并实现取消令牌以立即停止处理。
    • 此外,FileAccessWatcher 类实例应仅在 RaiseFileFinishedCopyingEvent 完成后将其自身从字典中删除,以防其处理导致文件重新添加到字典中。
    【解决方案7】:

    您可以使用带有虚拟变量的 lock 语句,它似乎工作得很好。

    检查here

    【讨论】:

      【解决方案8】:

      使用@Gordon Thompson 的答案,您必须创建一个循环,如下面的代码:

      public static bool IsFileReady(string sFilename)
      {
          try
          {
              using (FileStream inputStream = File.Open(sFilename, FileMode.Open, FileAccess.Read, FileShare.None))
                  return inputStream.Length > 0;
          }
          catch (Exception)
          {
              return false;
          }
      }
      
      while (!IsFileReady(yourFileName)) ;
      

      我找到了一种不会导致 CPU 开销的优化方式:

      public static bool IsFileReady(this string sFilename)
      {
          try
          {
              using (FileStream inputStream = File.Open(sFilename, FileMode.Open, FileAccess.Read, FileShare.None))
                  return inputStream.Length > 0;
          }
          catch (Exception)
          {
              return false;
          }
      }
      
      SpinWait.SpinUntil(yourFileName.IsFileReady);
      

      【讨论】:

      【解决方案9】:

      在上面的答案中我写了一个类似的,但它是异步的、非阻塞的、可等待的、可取消的(只需停止任务)并检查抛出的异常。

      public static async Task IsFileReady(string filename)
          {
              await Task.Run(() =>
              {
                  if (!File.Exists(path))
                  {
                      throw new IOException("File does not exist!");
                  }
      
                  var isReady = false;
      
                  while (!isReady)
                  {
                      // If the file can be opened for exclusive access it means that the file
                      // is no longer locked by another process.
                      try
                      {
                          using (FileStream inputStream =
                              File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.None))
                              isReady = inputStream.Length > 0;
                      }
                      catch (Exception e)
                      {
                          // Check if the exception is related to an IO error.
                          if (e.GetType() == typeof(IOException))
                          {
                              isReady = false;
                          }
                          else
                          {
                              // Rethrow the exception as it's not an exclusively-opened-exception.
                              throw;
                          }
                      }
                  }
              });
          }
      

      你可以这样使用它:

      Task ready = IsFileReady(path);
      
      ready.Wait(1000);
      
      if (!ready.IsCompleted)
      {
          throw new FileLoadException($"The file {path} is exclusively opened by another process!");
      }
      
      File.Delete(path);
      

      如果你真的要等待它,或者以更 JS-promise 的方式:

      IsFileReady(path).ContinueWith(t => File.Delete(path));
      

      【讨论】:

        【解决方案10】:

        问题是您的代码已经通过调用File.Create 打开文件,这将返回一个打开的文件流。根据时间的不同,垃圾收集器可能已经注意到返回的流未使用并将其放入终结器队列,然后终结器线程可能在您再次开始写入文件之前已经清理了所有内容。但这并不能保证,正如您所注意到的。

        要修复它,您可以像File.Create(...).Dispose() 一样立即再次关闭文件。或者,将流包装在 using 语句中,然后写入。

        using (FileStream stream = File.Create(fileName))
        using (Bitmap ss = new Bitmap(bounds.Width, bounds.Height))
        using (Graphics g = Graphics.FromImage(ss))
        {
            g.CopyFromScreen(whichForm.Location, Point.Empty, bounds.Size);
            ss.Save(stream, ImageFormat.Jpeg);
        }
        

        【讨论】:

          【解决方案11】:

          我使用的一种做法是在文件的字符串末尾写一个特定的单词。 键入“退出”。 然后检查读取的字符串是否以单词“Exit”结尾意味着文件已被完全读取。

          【讨论】:

          • 添加return语句也解决了为什么使用Exit代替的问题?
          猜你喜欢
          • 1970-01-01
          • 2015-01-25
          • 1970-01-01
          • 2019-03-25
          • 1970-01-01
          • 2016-11-05
          • 2019-08-24
          • 2010-10-15
          • 2018-11-08
          相关资源
          最近更新 更多