Dosyanın .NET'te kilidi açılıncaya kadar bekleyin


103

Bir dosyanın kilidi açılana ve okuma ve yeniden adlandırma için erişilebilir olana kadar bir iş parçacığını engellemenin en basit yolu nedir? Örneğin, .NET Framework'te bir yerde WaitOnFile () var mı?

Bir FTP sitesine iletilecek dosyaları aramak için FileSystemWatcher kullanan bir hizmetim var, ancak dosya olay, diğer işlem dosyayı yazmayı bitirmeden önce tetikleniyor.

İdeal çözümün bir zaman aşımı süresi olacaktır, böylece iş parçacığı vazgeçmeden önce sonsuza kadar asılı kalmaz.

Düzenleme: Aşağıdaki çözümlerden bazılarını denedikten sonra, sistemi tüm dosyaların yazması için değiştirdim Path.GetTempFileName(), ardından File.Move()son konuma bir gerçekleştirdim. Yakında Olarak FileSystemWatcherolay ateş, dosya zaten tamamlanmıştı.


4
.NET 4.0'ın yayımlanmasından bu yana, bu sorunu çözmenin daha iyi bir yolu var mı?
jason

Yanıtlar:


40

İlgili soruya verdiğim cevap buydu :

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

8
Bunu çirkin buluyorum ama mümkün olan tek çözüm
knoopx

6
Bu genel durumda gerçekten işe yarayacak mı? Eğer dosyayı bir using () yan tümcesinde açarsanız, kullanım kapsamı sona erdiğinde dosya kapatılır ve kilidi açılır. Bununla aynı stratejiyi kullanan ikinci bir işlem varsa (tekrar tekrar deneyin), WaitForFile () çıkışından sonra dosyanın açılıp açılmayacağına ilişkin bir yarış koşulu vardır. Hayır?
Cheeso

75
Kötü bir fikir! Kavram doğru olsa da, bir bool yerine FileStream'i döndürmek daha iyi bir çözüm olacaktır. Kullanıcı dosya üzerindeki kilidini alma şansı bulamadan dosya tekrar kilitlenirse - işlev "false"
döndürse

2
Fero'nun yöntemi nerede?
Vbp

1
Nissim'in yorumu tam olarak benim de düşündüğüm şeydi ama eğer bu aramayı kullanacaksanız, baytı okuduktan sonra onu 0'a sıfırlamayı unutmayın. fs.Seek (0, SeekOrigin.Begin);
whol

73

Eric'in cevabından başlayarak, kodu çok daha kompakt ve yeniden kullanılabilir hale getirmek için bazı iyileştirmeler ekledim. Umarım yararlıdır.

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

17
Gelecekten bu kodun hala bir cazibe gibi çalıştığını söylemek için geldim. Teşekkürler.
OnoSendai

6
@PabloCosta Kesinlikle! O olamaz amacı yenerek, bu olmamış olsa iyi olur, bunu kapatmak içinde başka bir iş parçacığı kudreti ırk ve açın. Bu uygulama doğrudur çünkü açık tutar! Arayanın bunun için endişelenmesine izin verin, boşta olması güvenlidir using, sadece usingbloğun içinde boş değeri kontrol edin .
doug65536

2
"FileStream fs = null;" try dışında ancak for içinde beyan edilmelidir. Sonra try içinde fs atayın ve kullanın. Catch bloğu "if (fs! = Null) fs.Dispose ();" (veya C # 6'da sadece fs? .Dispose ()) döndürülmeyen FileStream'in düzgün bir şekilde temizlendiğinden emin olmak için.
Bill Menees

1
Bir bayt okumak gerçekten gerekli mi? Tecrübelerime göre, dosyayı okuma erişimi için açtıysanız, ona sahipsiniz, test etmenize gerek yok. Buradaki tasarımla özel erişimi zorlamasanız da, ilk baytı okuyabilirsiniz ancak diğerlerini okuyamazsınız (bayt seviyesinde kilitleme). Orijinal sorudan, salt okunur paylaşım düzeyiyle açmanız olasıdır, bu nedenle başka hiçbir işlem dosyayı kilitleyemez veya değiştiremez. Her halükarda, kullanıma bağlı olarak fs.ReadByte () 'ın ya tam bir israf olduğunu ya da yeterli olmadığını hissediyorum.
eselk

8
Kullanıcı blokta hangi durum fsboş olamaz catch? Eğer FileStreamyapıcı atar, değişken bir değer atanmaz ve içeride başka bir şey trybir atabilir IOException. Bana göre sadece yapmanın iyi olması gerekiyor return new FileStream(...).
Matti Virkkunen

18

Dosya işleminin kendisinden bağımsız olarak, bunu yapmak için genel bir kod. Bu, nasıl kullanılacağına dair bir örnektir:

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

veya

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

Yeniden deneme sayısını ve yeniden denemeler arasındaki bekleme süresini de tanımlayabilirsiniz.

NOT: Ne yazık ki, temeldeki Win32 hatası (ERROR_SHARING_VIOLATION) .NET ile açığa çıkmaz, bu yüzden bunu IsSharingViolationkontrol etmek için yansıtma mekanizmalarına dayalı küçük bir hack işlevi ( ) ekledim .

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

5
Gerçekten bir SharingViolationException. Aslında, aşağıya doğru indiği sürece geriye uyumlu olarak hala yapabilirler.IOException . Ve gerçekten, gerçekten yapmalılar.
Roman Starkov


9
.NET Framework 4.5, .NET Standard ve .NET Core'da HResult, Exception sınıfındaki genel bir özelliktir. Bunun için artık düşünmeye gerek yok. MSDN'den:Starting with the .NET Framework 4.5, the HResult property's setter is protected, whereas its getter is public. In previous versions of the .NET Framework, both getter and setter are protected.
NightOwl888

13

Bu tür şeyler için bir yardımcı sınıf hazırladım. Dosyaya erişecek her şey üzerinde kontrolünüz varsa çalışacaktır. Bir sürü başka şeyden çekişme bekliyorsanız, bu oldukça değersizdir.

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
}

Adlandırılmış bir muteks kullanarak çalışır. Dosyaya erişmek isteyenler, dosyanın adını paylaşan ('\' ler '/' lere dönüştürülmüş olarak) adlandırılmış muteksin kontrolünü ele geçirmeye çalışır. Muteks erişilebilir olana kadar duracak olan Open () kullanabilir ya da verilen süre boyunca muteksi elde etmeye çalışan ve zaman aralığı içinde elde edemezse false döndüren TryOpen (TimeSpan) kullanabilirsiniz. Bu, kilitlerin düzgün bir şekilde serbest bırakılmasını sağlamak için büyük olasılıkla bir kullanım bloğu içinde kullanılmalıdır ve bu nesne atıldığında akış (açıksa) uygun şekilde atılacaktır.

Dosyanın çeşitli okuma / yazma işlemlerini yapmak için ~ 20 şeyle hızlı bir test yaptım ve herhangi bir bozulma görmedim. Açıkçası çok gelişmiş değil, ancak basit durumların çoğunda işe yaraması gerekiyor.


5

Bu özel uygulama için, dosyayı doğrudan gözlemlemek, kaçınılmaz olarak, özellikle dosya boyutu arttığında, izlenmesi zor bir hataya yol açacaktır. İşte işe yarayacak iki farklı strateji.

  • Ftp iki dosya ama sadece birini izle. Örneğin, important.txt ve important.finish dosyalarını gönderin. Yalnızca bitiş dosyasını izleyin, ancak txt'yi işleyin.
  • Bir dosyayı FTP'ye koyun, ancak tamamlandığında yeniden adlandırın. Örneğin, önemli.wait'i gönderin ve tamamlandığında gönderenin adını önemli.txt olarak değiştirmesini sağlayın.

İyi şanslar!


Bu otomatikin tam tersidir. Bu, dosyayı daha fazla adımla manuel olarak almak gibidir.
HackSlash

4

Bir süre önce kullandığım tekniklerden biri kendi fonksiyonumu yazmaktı. Temel olarak istisnayı yakalayın ve belirli bir süre boyunca ateşleyebileceğiniz bir zamanlayıcı kullanarak yeniden deneyin. Daha iyi bir yol varsa lütfen paylaşın.


3

Gönderen MSDN :

OnCreated olayı, bir dosya oluşturulur oluşturulmaz ortaya çıkar. Bir dosya izlenen bir dizine kopyalanır veya aktarılırsa, OnCreated olayı hemen başlatılır ve ardından bir veya daha fazla OnChanged olayları gelir.

FileSystemWatcher, "OnCreated" olayı sırasında okumasını / yeniden adlandırmasını yapmayacak şekilde değiştirilebilir, bunun yerine:

  1. Dosya durumunu kilitlenene kadar sorgulayan bir iş parçacığı yayar (bir FileInfo nesnesi kullanarak)
  2. Dosyanın artık kilitli olmadığını ve kullanıma hazır olduğunu belirlediği anda dosyayı işlemek için hizmeti geri çağırır

1
Dosya sistemi izleyicisinin iş parçacığını oluşturmak, temeldeki arabelleğin taşmasına neden olabilir, böylece birçok değiştirilmiş dosya kaybolabilir. Daha iyi bir yaklaşım, bir tüketici / üretici kuyruğu oluşturmak olacaktır.
Nissim

2

Çoğu durumda @harpo'nun önerdiği gibi basit bir yaklaşım işe yarayacaktır. Bu yaklaşımı kullanarak daha karmaşık kodlar geliştirebilirsiniz:

  • SystemHandleInformation \ SystemProcessInformation kullanılarak seçilen dosya için tüm açık tutamaçları bulun
  • Dahili tanıtıcısına erişim sağlamak için alt sınıf WaitHandle sınıfı
  • Alt sınıflandırılmış WaitHandle içinde sarılmış tutamaçları WaitHandle'a geçirin.

2

Dosya aktarımı tamamlandıktan sonra oluşturulan işlem tetikleyici dosyası SameNameASTrasferedFile.trg'yi aktarmak için reklam.

Ardından olayı yalnızca * .trg dosyasında tetikleyecek olan FileSystemWatcher'ı kurun.


1

Dosyanın kilit durumunu belirlemek için ne kullandığınızı bilmiyorum, ancak bunun gibi bir şey yapmalı.

while (true)
{
    Deneyin {
        stream = Dosya.Açık (dosyaAdı, dosyaModu);
        kırmak;
    }
    catch (FileIOException) {

        // bunun bir kilit sorunu olup olmadığını kontrol edin

        Thread.Sleep (100);
    }
}

1
Biraz geç, ancak dosya bir şekilde kilitlendiğinde döngünüzden asla çıkamazsınız. Bir sayaç eklemelisiniz (1. cevaba bakınız).
Peter

0

Olası bir çözüm, bir dosya sistemi izleyicisini bir oylama ile birleştirmek olabilir.

Bir Dosyadaki her Değişiklik için Bildirim Alın ve bildirim alırken, şu anda kabul edilen cevapta belirtildiği gibi kilitli olup olmadığını kontrol edin: https://stackoverflow.com/a/50800/6754146 Dosya akışını açma kodu cevaptan kopyalanır ve biraz değiştirildi:

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

Bu şekilde, bir dosyanın kilitli olup olmadığını kontrol edebilir ve belirtilen geri arama üzerinden kapatıldığında bildirim alabilirsiniz, bu şekilde aşırı agresif sorgulamadan kaçınır ve işi yalnızca gerçekten kapatılabileceği zaman yaparsınız.


-1

Gulzar ile aynı şekilde yapıyorum, sadece bir döngü ile denemeye devam et.

Aslında dosya sistemi izleyicisiyle bile uğraşmıyorum. Bir ağ sürücüsünü dakikada bir yeni dosyalar için yoklamak ucuzdur.


2
Ucuz olabilir, ancak birçok uygulama için dakikada bir çok uzun. Gerçek zamanlı izleme bazen çok önemlidir. C # 'da Dosya Sistemi mesajlarını dinleyecek bir şey uygulamak zorunda kalmak yerine (bunlar için en uygun dil değil) FSW kullanırsınız.
ThunderGr

-1

NotifyFilter NotifyFilters.LastWrite ile Changed olayını kullanmanız yeterlidir :

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

1
FileSystemWatcher yalnızca bir dosyaya yazılması bittiğinde bildirimde bulunmaz. Genellikle "tek" mantıksal yazma için sizi birkaç kez bilgilendirir ve ilk bildirimi aldıktan sonra dosyayı açmaya çalışırsanız bir istisna alırsınız.
Ross 13

-1

Görünüm eki eklerken benzer bir sorunla karşılaştım. "Kullanarak" günü kurtardı.

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

-3

Buna bir seçenek olarak ne dersiniz:

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

Elbette dosya boyutu yaratımda önceden tahsis edilmişse yanlış bir pozitif alırsınız.


1
Dosyaya yazma işlemi bir saniyeden daha uzun bir süre duraklarsa veya bellekte bir saniyeden daha uzun süre tamponlarsa, başka bir yanlış pozitif alırsınız. Bunun hiçbir koşulda iyi bir çözüm olduğunu düşünmüyorum.
Chris Wenham
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.