2008-09-08 36 views
79

Cách đơn giản nhất để chặn luồng cho đến khi tệp được mở khóa và có thể truy cập để đọc và đổi tên? Ví dụ, có một WaitOnFile() ở đâu đó trong Khuôn khổ .NET?Đợi cho đến khi tệp được mở khóa trong .NET

Tôi có dịch vụ sử dụng FileSystemWatcher để tìm tệp được truyền tới trang FTP, nhưng tệp đã tạo sự kiện kích hoạt trước khi quá trình khác hoàn tất ghi tệp.

Giải pháp lý tưởng sẽ có khoảng thời gian chờ để chuỗi không treo mãi mãi trước khi bỏ cuộc.

Edit: Sau khi cố gắng ra một số giải pháp dưới đây, tôi đã kết thúc việc thay đổi hệ thống để tất cả các file đã viết thư cho Path.GetTempFileName(), sau đó thực hiện một File.Move() đến vị trí chính thức. Ngay sau khi sự kiện FileSystemWatcher được kích hoạt, tệp đã hoàn tất.

+4

Kể từ khi phát hành .NET 4.0, có cách nào tốt hơn để giải quyết vấn đề này không vấn đề? – jason

+1

https://connect.microsoft.com/VisualStudio/feedback/details/670033/provide-an-overload-of-file-open-and-or-filestream-constructor-that-blocks-until-file-is-unlocked – Timwi

Trả lời

31

Đây là câu trả lời tôi đã vào một related question:

/// <summary> 
    /// Blocks until the file is not locked any more. 
    /// </summary> 
    /// <param name="fullPath"></param> 
    bool WaitForFile(string fullPath) 
    { 
     int numTries = 0; 
     while (true) 
     { 
      ++numTries; 
      try 
      { 
       // Attempt to open the file exclusively. 
       using (FileStream fs = new FileStream(fullPath, 
        FileMode.Open, FileAccess.ReadWrite, 
        FileShare.None, 100)) 
       { 
        fs.ReadByte(); 

        // If we got this far the file is ready 
        break; 
       } 
      } 
      catch (Exception ex) 
      { 
       Log.LogWarning(
        "WaitForFile {0} failed to get an exclusive lock: {1}", 
        fullPath, ex.ToString()); 

       if (numTries > 10) 
       { 
        Log.LogWarning(
         "WaitForFile {0} giving up after 10 tries", 
         fullPath); 
        return false; 
       } 

       // Wait for the lock to be released 
       System.Threading.Thread.Sleep(500); 
      } 
     } 

     Log.LogTrace("WaitForFile {0} returning true after {1} tries", 
      fullPath, numTries); 
     return true; 
    } 
+6

Tôi thấy điều này xấu xí nhưng giải pháp duy nhất có thể – knoopx

+4

Điều này thực sự sẽ làm việc trong trường hợp chung? nếu bạn mở tệp bằng mệnh đề using(), tệp sẽ bị đóng và mở khóa khi phạm vi sử dụng kết thúc. Nếu có một quá trình thứ hai sử dụng cùng một chiến lược như thế này (thử lại nhiều lần), sau đó sau khi thoát khỏi WaitForFile(), có một điều kiện chủng tộc liên quan đến việc liệu tệp đó có thể mở được hay không. Không? – Cheeso

+0

Bạn đang nói về 2 chủ đề trong cùng một ứng dụng gọi WaitForFile trên cùng một tệp? Hmm, không chắc chắn, vì tôi chủ yếu sử dụng điều này để chờ đợi cho các quá trình khác để cho đi của tập tin. Tôi đã có mã này trong sản xuất trong một thời gian dài và nó đã làm việc tốt cho tôi. Nên khá đơn giản để viết một ứng dụng để kiểm tra lý thuyết của bạn. –

4

Một trong những kỹ thuật tôi đã sử dụng một thời gian nữa là viết chức năng của riêng mình. Về cơ bản, hãy nắm bắt ngoại lệ và thử lại bằng bộ hẹn giờ bạn có thể kích hoạt trong một khoảng thời gian cụ thể. Nếu có cách nào tốt hơn, hãy chia sẻ.

1

Tôi không biết những gì bạn đang sử dụng để xác định trạng thái khóa của tệp, nhưng một cái gì đó như thế này nên làm điều đó.

 
while (true) 
{ 
    try { 
     stream = File.Open(fileName, fileMode); 
     break; 
    } 
    catch(FileIOException) { 

     // check whether it's a lock problem 

     Thread.Sleep(100); 
    } 
} 
3

Từ MSDN:

Sự kiện OnCreated được nâng lên trong thời gian sớm như một tập tin được tạo ra. Nếu một tệp là đang được sao chép hoặc chuyển vào thư mục đã xem , sự kiện OnCreated sẽ được tăng lên ngay lập tức, theo sau bởi một hoặc nhiều sự kiện OnChanged.

FileSystemWatcher của bạn có thể được sửa đổi để nó không làm đọc nó/đổi tên trong sự kiện "OnCreated", mà đúng hơn:

  1. Spanws a thread mà các cuộc thăm dò tình trạng tập tin cho đến khi nó không phải là khóa (sử dụng một đối tượng FileInfo)
  2. cuộc gọi trở lại vào dịch vụ để xử lý các tập tin ngay sau khi nó xác định các tập tin không còn bị khóa và sẵn sàng để đi
+1

Việc sinh ra luồng của trình theo dõi hệ thống tập tin có thể dẫn đến tràn bộ đệm cơ bản, do đó thiếu nhiều tệp đã thay đổi. Một cách tiếp cận tốt hơn là tạo hàng đợi người tiêu dùng/nhà sản xuất. – Nissim

-1

Tôi làm theo cách tương tự như Gulzar, chỉ cần tiếp tục thử với một vòng lặp.

Trong thực tế, tôi thậm chí không bận tâm với trình xem hệ thống tệp. Thăm dò ổ đĩa mạng cho các tệp mới mỗi phút một lần là rẻ.

+0

Nó có thể rẻ nhưng một lần một phút là quá dài đối với nhiều ứng dụng. Giám sát thời gian thực là điều cần thiết đôi khi. Thay vì bạn phải thực hiện một cái gì đó sẽ được lắng nghe cho các tin nhắn hệ thống tập tin trong C# (không phải là ngôn ngữ thuận tiện nhất cho những điều này) bạn sử dụng FSW. – ThunderGr

2

Trong hầu hết các trường hợp, cách tiếp cận đơn giản như @harpo được đề xuất sẽ hoạt động.Bạn có thể phát triển mã phức tạp hơn sử dụng phương pháp này:

  • Tìm tất cả các xử lý cho tập tin được lựa chọn sử dụng SystemHandleInformation \ SystemProcessInformation
  • Subclass lớp WaitHandle để đạt được quyền truy cập vào nó mở của xử lý nội bộ
  • đèo thấy tay cầm được bọc trong subclassed WaitHandle để WaitHandle.WaitAny phương pháp
5

Đối với ứng dụng cụ thể này, việc quan sát trực tiếp tệp chắc chắn sẽ dẫn đến lỗi khó theo dõi, đặc biệt khi kích thước tệp tăng lên. Dưới đây là hai chiến lược khác nhau sẽ hoạt động.

  • Ftp hai tệp nhưng chỉ xem một tệp. Ví dụ gửi các tập tin important.txt và important.finish. Chỉ xem cho tập tin kết thúc nhưng xử lý txt.
  • FTP một tệp nhưng đổi tên tệp đó khi hoàn tất. Ví dụ: gửi yêu cầu quan trọng và yêu cầu người gửi đổi tên thành important.txt khi hoàn tất.

Chúc may mắn!

-3

Làm thế nào về vấn đề này như một tùy chọn:

private void WaitOnFile(string fileName) 
{ 
    FileInfo fileInfo = new FileInfo(fileName); 
    for (long size = -1; size != fileInfo.Length; fileInfo.Refresh()) 
    { 
     size = fileInfo.Length; 
     System.Threading.Thread.Sleep(1000); 
    } 
} 

Tất nhiên nếu filesize được preallocated trên tạo bạn muốn có được một dương tính giả.

+1

Nếu quá trình ghi vào tập tin tạm dừng trong hơn một giây, hoặc bộ đệm trong bộ nhớ trong hơn một giây, thì bạn sẽ nhận được một số dương sai khác. Tôi không nghĩ đây là giải pháp tốt trong mọi hoàn cảnh. –

12

Tôi đã ném cùng một lớp trợ giúp cho những thứ này. Nó sẽ hoạt động nếu bạn có quyền kiểm soát mọi thứ có thể truy cập tệp. Nếu bạn đang mong đợi tranh chấp từ một loạt các thứ khác, thì điều này là vô giá trị.

using System; 
using System.IO; 
using System.Threading; 

/// <summary> 
/// This is a wrapper aroung a FileStream. While it is not a Stream itself, it can be cast to 
/// one (keep in mind that this might throw an exception). 
/// </summary> 
public class SafeFileStream: IDisposable 
{ 
    #region Private Members 
    private Mutex m_mutex; 
    private Stream m_stream; 
    private string m_path; 
    private FileMode m_fileMode; 
    private FileAccess m_fileAccess; 
    private FileShare m_fileShare; 
    #endregion//Private Members 

    #region Constructors 
    public SafeFileStream(string path, FileMode mode, FileAccess access, FileShare share) 
    { 
     m_mutex = new Mutex(false, String.Format("Global\\{0}", path.Replace('\\', '/'))); 
     m_path = path; 
     m_fileMode = mode; 
     m_fileAccess = access; 
     m_fileShare = share; 
    } 
    #endregion//Constructors 

    #region Properties 
    public Stream UnderlyingStream 
    { 
     get 
     { 
      if (!IsOpen) 
       throw new InvalidOperationException("The underlying stream does not exist - try opening this stream."); 
      return m_stream; 
     } 
    } 

    public bool IsOpen 
    { 
     get { return m_stream != null; } 
    } 
    #endregion//Properties 

    #region Functions 
    /// <summary> 
    /// Opens the stream when it is not locked. If the file is locked, then 
    /// </summary> 
    public void Open() 
    { 
     if (m_stream != null) 
      throw new InvalidOperationException(SafeFileResources.FileOpenExceptionMessage); 
     m_mutex.WaitOne(); 
     m_stream = File.Open(m_path, m_fileMode, m_fileAccess, m_fileShare); 
    } 

    public bool TryOpen(TimeSpan span) 
    { 
     if (m_stream != null) 
      throw new InvalidOperationException(SafeFileResources.FileOpenExceptionMessage); 
     if (m_mutex.WaitOne(span)) 
     { 
      m_stream = File.Open(m_path, m_fileMode, m_fileAccess, m_fileShare); 
      return true; 
     } 
     else 
      return false; 
    } 

    public void Close() 
    { 
     if (m_stream != null) 
     { 
      m_stream.Close(); 
      m_stream = null; 
      m_mutex.ReleaseMutex(); 
     } 
    } 

    public void Dispose() 
    { 
     Close(); 
     GC.SuppressFinalize(this); 
    } 

    public static explicit operator Stream(SafeFileStream sfs) 
    { 
     return sfs.UnderlyingStream; 
    } 
    #endregion//Functions 
} 

Nó hoạt động bằng cách sử dụng một mutex được đặt tên. Những người muốn truy cập tập tin cố gắng để có được quyền kiểm soát của mutex được đặt tên, trong đó chia sẻ tên của tập tin (với '\' s biến thành '/' s). Bạn có thể sử dụng Open(), sẽ dừng cho đến khi có thể truy cập được mutex hoặc bạn có thể sử dụng TryOpen (TimeSpan), cố gắng thu được mutex trong khoảng thời gian đã cho và trả về false nếu nó không thể có được trong khoảng thời gian đó. Điều này rất có thể sẽ được sử dụng bên trong một khối sử dụng, để đảm bảo rằng khóa được giải phóng đúng cách và luồng (nếu mở) sẽ được xử lý đúng cách khi vật thể này được xử lý.

Tôi đã làm một bài kiểm tra nhanh với ~ 20 điều cần làm nhiều lần đọc/ghi khác nhau của tệp và không thấy tham nhũng. Rõ ràng nó không phải là rất tiên tiến, nhưng nó sẽ làm việc cho phần lớn các trường hợp đơn giản.

+0

Giải pháp tuyệt vời ... cảm ơn! – Nissim

51

Bắt đầu từ câu trả lời của Eric, tôi đã bao gồm một số cải tiến để làm cho mã trở nên nhỏ gọn và dễ sử dụng hơn. Hy vọng nó hữu ích.

FileStream WaitForFile (string fullPath, FileMode mode, FileAccess access, FileShare share) 
{ 
    for (int numTries = 0; numTries < 10; numTries++) { 
     FileStream fs = null; 
     try { 
      fs = new FileStream (fullPath, mode, access, share); 
      return fs; 
     } 
     catch (IOException) { 
      if (fs != null) { 
       fs.Dispose(); 
      } 
      Thread.Sleep (50); 
     } 
    } 

    return null; 
} 
+9

Tôi đến từ tương lai để nói rằng mã này vẫn hoạt động như một sự quyến rũ. Cảm ơn. – OnoSendai

+0

Hoạt động rất tốt. Nhưng bạn không đóng luồng, do đó, bạn có thể gặp phải vấn đề khi nhập các tệp đó sau (ví dụ: nếu bạn định xóa chúng sau khi đọc). –

+0

@PabloCosta Người gọi có thể chỉ cần bọc nó trong khối 'using'. Tất nhiên bạn không thể quên đi dòng. – mafu

15

Đây là mã chung để thực hiện việc này, độc lập với chính hoạt động của tệp. Đây là một ví dụ về cách sử dụng nó:

WrapSharingViolations(() => File.Delete(myFile)); 

hoặc

WrapSharingViolations(() => File.Copy(mySourceFile, myDestFile)); 

Bạn cũng có thể xác định số lượng retry, và thời gian chờ đợi giữa retries.

LƯU Ý: Thật không may, lỗi Win32 cơ bản (ERROR_SHARING_VIOLATION) không được hiển thị.NET, vì vậy tôi đã thêm một chức năng hack nhỏ (IsSharingViolation) dựa trên cơ chế phản chiếu để kiểm tra điều này.

/// <summary> 
    /// Wraps sharing violations that could occur on a file IO operation. 
    /// </summary> 
    /// <param name="action">The action to execute. May not be null.</param> 
    public static void WrapSharingViolations(WrapSharingViolationsCallback action) 
    { 
     WrapSharingViolations(action, null, 10, 100); 
    } 

    /// <summary> 
    /// Wraps sharing violations that could occur on a file IO operation. 
    /// </summary> 
    /// <param name="action">The action to execute. May not be null.</param> 
    /// <param name="exceptionsCallback">The exceptions callback. May be null.</param> 
    /// <param name="retryCount">The retry count.</param> 
    /// <param name="waitTime">The wait time in milliseconds.</param> 
    public static void WrapSharingViolations(WrapSharingViolationsCallback action, WrapSharingViolationsExceptionsCallback exceptionsCallback, int retryCount, int waitTime) 
    { 
     if (action == null) 
      throw new ArgumentNullException("action"); 

     for (int i = 0; i < retryCount; i++) 
     { 
      try 
      { 
       action(); 
       return; 
      } 
      catch (IOException ioe) 
      { 
       if ((IsSharingViolation(ioe)) && (i < (retryCount - 1))) 
       { 
        bool wait = true; 
        if (exceptionsCallback != null) 
        { 
         wait = exceptionsCallback(ioe, i, retryCount, waitTime); 
        } 
        if (wait) 
        { 
         System.Threading.Thread.Sleep(waitTime); 
        } 
       } 
       else 
       { 
        throw; 
       } 
      } 
     } 
    } 

    /// <summary> 
    /// Defines a sharing violation wrapper delegate. 
    /// </summary> 
    public delegate void WrapSharingViolationsCallback(); 

    /// <summary> 
    /// Defines a sharing violation wrapper delegate for handling exception. 
    /// </summary> 
    public delegate bool WrapSharingViolationsExceptionsCallback(IOException ioe, int retry, int retryCount, int waitTime); 

    /// <summary> 
    /// Determines whether the specified exception is a sharing violation exception. 
    /// </summary> 
    /// <param name="exception">The exception. May not be null.</param> 
    /// <returns> 
    ///  <c>true</c> if the specified exception is a sharing violation exception; otherwise, <c>false</c>. 
    /// </returns> 
    public static bool IsSharingViolation(IOException exception) 
    { 
     if (exception == null) 
      throw new ArgumentNullException("exception"); 

     int hr = GetHResult(exception, 0); 
     return (hr == -2147024864); // 0x80070020 ERROR_SHARING_VIOLATION 

    } 

    /// <summary> 
    /// Gets the HRESULT of the specified exception. 
    /// </summary> 
    /// <param name="exception">The exception to test. May not be null.</param> 
    /// <param name="defaultValue">The default value in case of an error.</param> 
    /// <returns>The HRESULT value.</returns> 
    public static int GetHResult(IOException exception, int defaultValue) 
    { 
     if (exception == null) 
      throw new ArgumentNullException("exception"); 

     try 
     { 
      const string name = "HResult"; 
      PropertyInfo pi = exception.GetType().GetProperty(name, BindingFlags.NonPublic | BindingFlags.Instance); // CLR2 
      if (pi == null) 
      { 
       pi = exception.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance); // CLR4 
      } 
      if (pi != null) 
       return (int)pi.GetValue(exception, null); 
     } 
     catch 
     { 
     } 
     return defaultValue; 
    } 
+2

Họ thực sự có thể đã cung cấp một 'SharingViolationException'. Trong thực tế, chúng vẫn có thể, ngược lại, tương thích, miễn là nó xuất phát từ 'IOException'. Và họ thực sự, thực sự nên. –

+5

Marshal.GetHRForException http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal.gethrforexception.aspx –

+1

Trong .NET Framework 4.5, .NET Standard và .NET Core, HResult là một tài sản công cộng trên lớp Ngoại lệ. Phản ánh không còn cần thiết cho việc này nữa. Từ MSDN: 'Bắt đầu với .NET Framework 4.5, thiết lập của thuộc tính HResult được bảo vệ, trong khi bộ khởi động của nó là công khai. Trong các phiên bản trước của .NET Framework, cả getter và setter đều được bảo vệ.' – NightOwl888

2

Quảng cáo để chuyển tệp trình kích hoạt quá trình SameNameASTrasferedFile.trg được tạo sau khi truyền tệp xong.

Sau đó, thiết lập FileSystemWatcher sẽ chỉ kích hoạt sự kiện trên tệp * .trg.

-1

Đơn giản chỉ cần sử dụng sự kiện Changed với NotifyFilter NotifyFilters.LastWrite:

var watcher = new FileSystemWatcher { 
     Path = @"c:\temp\test", 
     Filter = "*.xml", 
     NotifyFilter = NotifyFilters.LastWrite 
}; 
watcher.Changed += watcher_Changed; 
watcher.EnableRaisingEvents = true; 
+1

FileSystemWatcher không chỉ thông báo khi một tập tin được ghi xong. Nó sẽ thường xuyên thông báo cho bạn nhiều lần cho một bản ghi logic "đơn", và nếu bạn cố mở tệp sau khi nhận được thông báo đầu tiên, bạn sẽ nhận được một ngoại lệ. – Ross

-1

Tôi chạy vào một vấn đề tương tự khi thêm một tập tin đính kèm triển vọng. "Sử dụng" đã lưu trong ngày.

string fileName = MessagingBLL.BuildPropertyAttachmentFileName(currProp); 

       //create a temporary file to send as the attachment 
       string pathString = Path.Combine(Path.GetTempPath(), fileName); 

       //dirty trick to make sure locks are released on the file. 
       using (System.IO.File.Create(pathString)) { } 

       mailItem.Subject = MessagingBLL.PropertyAttachmentSubject; 
       mailItem.Attachments.Add(pathString, Outlook.OlAttachmentType.olByValue, Type.Missing, Type.Missing); 
0

Một giải pháp có thể sẽ là, kết hợp một FileSystemWatcher với một số điểm bỏ phiếu,

Nhận thông báo cho tất cả các thay đổi trên tập tin, và khi nhận được séc thông báo nếu nó được khóa như đã nêu trong chấp nhận hiện nay câu trả lời: https://stackoverflow.com/a/50800/6754146 mã cho mở FileStream được sao chép từ các câu trả lời và sửa đổi một chút:

public static void CheckFileLock(string directory, string filename, Func<Task> callBack) 
{ 
    var watcher = new FileSystemWatcher(directory, filename); 
    FileSystemEventHandler check = 
     async (sender, eArgs) => 
    { 
     string fullPath = Path.Combine(directory, filename); 
     try 
     { 
      // Attempt to open the file exclusively. 
      using (FileStream fs = new FileStream(fullPath, 
        FileMode.Open, FileAccess.ReadWrite, 
        FileShare.None, 100)) 
      { 
       fs.ReadByte(); 
       watcher.EnableRaisingEvents = false; 
       // If we got this far the file is ready 
      } 
      watcher.Dispose(); 
      await callBack(); 
     } 
     catch (IOException) { } 
    }; 
    watcher.NotifyFilter = NotifyFilters.LastWrite; 
    watcher.IncludeSubdirectories = false; 
    watcher.EnableRaisingEvents = true; 
    //Attach the checking to the changed method, 
    //on every change it gets checked once 
    watcher.Changed += check; 
    //Initially do a check for the case it is already released 
    check(null, null); 
} 

với cách này bạn có thể kiểm tra cho một tập tin nếu nhốt nó một d được thông báo khi đóng cửa trên cuộc gọi lại được chỉ định, cách này bạn tránh được việc bỏ phiếu quá tích cực và chỉ thực hiện công việc khi nó thực sự bị đóng

Các vấn đề liên quan