Tunggu hingga file dibuka kuncinya di .NET

103

Apa cara paling sederhana untuk memblokir utas hingga file dibuka kuncinya dan dapat diakses untuk membaca dan mengganti nama? Misalnya, apakah ada WaitOnFile () di suatu tempat di .NET Framework?

Saya memiliki layanan yang menggunakan FileSystemWatcher untuk mencari file yang akan dikirim ke situs FTP, tetapi peristiwa file yang dibuat akan aktif sebelum proses lain selesai menulis file.

Solusi ideal akan memiliki periode waktu tunggu sehingga utas tidak menggantung selamanya sebelum menyerah.

Sunting: Setelah mencoba beberapa solusi di bawah ini, saya akhirnya mengubah sistem sehingga semua file menulis Path.GetTempFileName(), kemudian melakukan File.Move()ke lokasi akhir. Begitu FileSystemWatcheracara dipicu, file itu sudah lengkap.

Chris Wenham
sumber
4
Sejak rilis .NET 4.0, apakah ada cara yang lebih baik untuk mengatasi masalah ini?
jason

Jawaban:

40

Inilah jawaban yang saya berikan untuk pertanyaan terkait :

    /// <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;
    }
Eric Z Beard
sumber
8
Saya menemukan ini jelek tetapi satu-satunya solusi yang mungkin
knoopx
6
Apakah ini benar-benar akan berhasil dalam kasus umum? jika Anda membuka file dalam klausa using (), file tersebut ditutup dan dibuka kuncinya saat cakupan penggunaan berakhir. Jika ada proses kedua yang menggunakan strategi yang sama seperti ini (coba lagi berulang kali), maka setelah keluar dari WaitForFile (), ada kondisi balapan mengenai apakah file tersebut dapat dibuka atau tidak. Tidak?
Cheeso
75
Ide buruk! Meskipun konsepnya benar, solusi yang lebih baik adalah mengembalikan FileStream daripada bool. Jika file dikunci lagi sebelum pengguna mendapat kesempatan untuk mendapatkan pengunciannya pada file - dia akan mendapatkan pengecualian bahkan jika fungsi mengembalikan "false"
Nissim
2
dimana metode Fero?
Vbp
1
Komentar Nissim persis seperti yang saya pikirkan juga, tetapi jika Anda akan menggunakan seek itu, jangan lupa untuk mengatur ulang ke 0 setelah membaca byte. fs.Seek (0, SeekOrigin.Begin);
WHol
73

Berawal dari jawaban Eric, saya menyertakan beberapa perbaikan untuk membuat kode jauh lebih kompak dan dapat digunakan kembali. Semoga bermanfaat.

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;
}
mafu
sumber
16
Saya datang dari masa depan untuk mengatakan bahwa kode ini masih berfungsi dengan baik. Terima kasih.
OnoSendai
6
@PabloCosta Persis! Itu tidak dapat menutupnya, karena jika itu terjadi, utas lain mungkin berlomba masuk dan membukanya, mengalahkan tujuan. Penerapan ini benar karena membuatnya tetap terbuka! Biarkan pemanggil khawatir tentang itu, aman untuk usingmenggunakan null, cukup periksa null di dalam usingblok.
doug65536
2
"FileStream fs = null;" harus dideklarasikan di luar percobaan tetapi di dalam untuk. Kemudian tetapkan dan gunakan fs dalam percobaan. Blok catch harus melakukan "if (fs! = Null) fs.Dispose ();" (atau hanya fs? .Dispose () di C # 6) untuk memastikan FileStream yang tidak dikembalikan dibersihkan dengan benar.
Bill Menees
1
Apakah benar-benar perlu membaca satu byte? Menurut pengalaman saya, jika Anda telah membuka file untuk akses baca, Anda memilikinya, tidak perlu mengujinya. Meskipun dengan desain di sini Anda tidak memaksakan akses eksklusif sehingga mungkin saja Anda dapat membaca byte pertama, tetapi tidak untuk yang lain (penguncian level byte). Dari pertanyaan awal Anda kemungkinan besar akan membuka dengan tingkat berbagi hanya-baca, jadi tidak ada proses lain yang dapat mengunci atau mengubah file. Bagaimanapun, saya merasa fs.ReadByte () benar-benar sia-sia, atau tidak cukup, tergantung pada penggunaan.
eselk
8
Pengguna yang keadaannya fstidak boleh null di catchblokir? Jika FileStreamkonstruktor melempar, variabel tidak akan diberi nilai, dan tidak ada hal lain di dalam tryyang bisa melempar IOException. Bagi saya sepertinya tidak apa-apa untuk melakukannya return new FileStream(...).
Matti Virkkunen
18

Berikut adalah kode umum untuk melakukan ini, terlepas dari operasi file itu sendiri. Ini adalah contoh bagaimana menggunakannya:

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

atau

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

Anda juga dapat menentukan jumlah percobaan ulang, dan waktu tunggu di antara percobaan ulang.

CATATAN: Sayangnya, kesalahan Win32 yang mendasari (ERROR_SHARING_VIOLATION) tidak terekspos dengan NET, jadi saya telah menambahkan fungsi hack kecil ( IsSharingViolation) berdasarkan mekanisme refleksi untuk memeriksa ini.

    /// <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;
    }
Simon Mourier
sumber
5
Mereka benar-benar bisa menyediakan file SharingViolationException. Faktanya, mereka masih bisa, mundur-kompatibel, asalkan diturunkan IOException. Dan mereka benar-benar harus.
Roman Starkov
6
Marshal.GetHRForException msdn.microsoft.com/en-us/library/…
Steven T. Cramer
9
Di .NET Framework 4.5, .NET Standard, dan .NET Core, HResult adalah properti publik di kelas Exception. Refleksi tidak lagi diperlukan untuk ini. Dari MSDN: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

Saya mengadakan kelas pembantu untuk hal-hal semacam ini. Ini akan berfungsi jika Anda memiliki kendali atas semua yang akan mengakses file. Jika Anda mengharapkan pertengkaran dari banyak hal lain, maka ini sangat tidak berharga.

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
}

Ia bekerja menggunakan mutex bernama. Mereka yang ingin mengakses file mencoba untuk mendapatkan kontrol dari mutex bernama, yang berbagi nama file (dengan '\' s berubah menjadi '/' s). Anda dapat menggunakan Open (), yang akan terhenti hingga mutex dapat diakses atau Anda dapat menggunakan TryOpen (TimeSpan), yang mencoba memperoleh mutex selama durasi tertentu dan mengembalikan false jika tidak dapat memperoleh dalam rentang waktu tersebut. Ini kemungkinan besar harus digunakan di dalam blok penggunaan, untuk memastikan bahwa kunci dilepaskan dengan benar, dan aliran (jika terbuka) akan dibuang dengan benar saat objek ini dibuang.

Saya melakukan tes cepat dengan ~ 20 hal untuk melakukan berbagai membaca / menulis file dan tidak melihat adanya korupsi. Jelas ini tidak terlalu maju, tetapi seharusnya berfungsi untuk sebagian besar kasus sederhana.

pengguna152791
sumber
5

Untuk aplikasi khusus ini, mengamati file secara langsung pasti akan menyebabkan bug yang sulit dilacak, terutama ketika ukuran file meningkat. Berikut adalah dua strategi berbeda yang akan berhasil.

  • Ftp dua file tetapi hanya menonton satu. Misalnya mengirim file important.txt dan important.finish. Hanya perhatikan file selesai tetapi proses txt.
  • FTP satu file tetapi ganti namanya setelah selesai. Misalnya kirim important.wait dan minta pengirim mengganti namanya menjadi important.txt setelah selesai.

Semoga berhasil!

jason saldo
sumber
Itu kebalikan dari otomatis. Itu seperti mendapatkan file secara manual, dengan lebih banyak langkah.
HackSlash
4

Salah satu teknik yang saya gunakan beberapa waktu lalu adalah menulis fungsi saya sendiri. Pada dasarnya tangkap pengecualian dan coba lagi menggunakan pengatur waktu yang dapat Anda aktifkan selama durasi tertentu. Jika ada cara yang lebih baik, silakan berbagi.

Gulzar Nazim
sumber
3

Dari MSDN :

Acara OnCreated dimunculkan segera setelah file dibuat. Jika file sedang disalin atau ditransfer ke direktori yang diawasi, acara OnCreated akan segera dimunculkan, diikuti oleh satu atau beberapa acara OnChanged.

FileSystemWatcher Anda dapat dimodifikasi sehingga tidak melakukan pembacaan / penggantian namanya selama acara "OnCreated", melainkan:

  1. Spanws utas yang memeriksa status file hingga tidak terkunci (menggunakan objek FileInfo)
  2. Panggilan kembali ke layanan untuk memproses file segera setelah ditentukan bahwa file tidak lagi terkunci dan siap digunakan
Guy Starbuck
sumber
1
Memunculkan utas pengamat sistem file dapat menyebabkan buffer yang mendasari meluap, sehingga kehilangan banyak file yang diubah. Pendekatan yang lebih baik adalah membuat antrian konsumen / produsen.
Nissim
2

Dalam kebanyakan kasus, pendekatan sederhana seperti yang disarankan @harpo akan berhasil. Anda dapat mengembangkan kode yang lebih canggih menggunakan pendekatan ini:

  • Temukan semua pegangan yang terbuka untuk file yang dipilih menggunakan SystemHandleInformation \ SystemProcessInformation
  • Subkelas WaitHandle untuk mendapatkan akses ke pegangan internalnya
  • Meneruskan pegangan yang ditemukan yang dibungkus dengan metode WaitHandle yang disubkelas ke WaitHandle.WaitAny
aku
sumber
2

Iklan untuk mentransfer file pemicu proses SameNameASTrasferedFile.trg yang dibuat setelah transmisi file selesai.

Kemudian siapkan FileSystemWatcher yang akan mengaktifkan peristiwa hanya pada file * .trg.

Rudi
sumber
1

Saya tidak tahu apa yang Anda gunakan untuk menentukan status kunci file, tetapi sesuatu seperti ini seharusnya melakukannya.

sementara (benar)
{
    coba {
        stream = File.Open (nama file, fileMode);
        istirahat;
    }
    catch (FileIOException) {

        // periksa apakah ini masalah kunci

        Thread.Sleep (100);
    }
}
harpo
sumber
1
Agak terlambat, tetapi ketika file terkunci, Anda tidak akan pernah keluar dari loop Anda. Anda harus menambahkan penghitung (lihat jawaban pertama).
Peter
0

Solusi yang mungkin adalah, menggabungkan pengamat sistem file dengan beberapa polling,

dapatkan Notifikasi untuk setiap Perubahan pada File, dan ketika mendapatkan notifikasi periksa apakah terkunci seperti yang dinyatakan dalam jawaban yang diterima saat ini: https://stackoverflow.com/a/50800/6754146 Kode untuk membuka filestream disalin dari jawaban dan sedikit dimodifikasi:

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

Dengan cara ini Anda dapat memeriksa file jika terkunci dan mendapatkan pemberitahuan ketika ditutup melalui callback yang ditentukan, dengan cara ini Anda menghindari polling yang terlalu agresif dan hanya melakukan pekerjaan ketika file tersebut mungkin benar-benar ditutup

Florian K
sumber
-1

Saya melakukannya dengan cara yang sama seperti Gulzar, teruslah mencoba dengan satu putaran.

Sebenarnya saya bahkan tidak peduli dengan pengamat sistem file. Memungut drive jaringan untuk file baru sekali dalam satu menit itu murah.

Jonathan Allen
sumber
2
Mungkin murah tapi sekali satu menit terlalu lama untuk banyak aplikasi. Pemantauan waktu nyata terkadang penting. Daripada Anda harus mengimplementasikan sesuatu yang akan mendengarkan pesan Filesystem di C # (bukan bahasa yang paling nyaman untuk hal-hal ini), Anda menggunakan FSW.
ThunderGr
-1

Cukup gunakan acara Berubah dengan NotifyFilter NotifyFilters.LastWrite :

var watcher = new FileSystemWatcher {
      Path = @"c:\temp\test",
      Filter = "*.xml",
      NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += watcher_Changed; 
watcher.EnableRaisingEvents = true;
Bernhard Hochgatterer
sumber
1
FileSystemWatcher tidak hanya memberi tahu saat file selesai ditulis. Ini akan sering memberi tahu Anda beberapa kali untuk penulisan logis "tunggal", dan jika Anda mencoba membuka file setelah menerima pemberitahuan pertama, Anda akan mendapatkan pengecualian.
Ross
-1

Saya mengalami masalah serupa saat menambahkan lampiran pandangan. "Menggunakan" menyelamatkan hari itu.

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);
Jahmal23
sumber
-3

Bagaimana kalau ini sebagai pilihan:

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

Tentu saja jika ukuran file dialokasikan sebelumnya pada pembuatan, Anda akan mendapatkan positif palsu.

Ralph Shillington
sumber
1
Jika proses penulisan ke file berhenti selama lebih dari satu detik, atau buffer dalam memori lebih dari satu detik, maka Anda akan mendapatkan positif palsu lainnya. Saya tidak berpikir ini adalah solusi yang baik dalam keadaan apa pun.
Chris Wenham