سؤال

ما هي أبسط طريقة لحظر سلسلة محادثات حتى يتم إلغاء قفل الملف ويمكن الوصول إليه للقراءة وإعادة التسمية؟على سبيل المثال، هل يوجد WaitOnFile() في مكان ما في .NET Framework؟

لدي خدمة تستخدم FileSystemWatcher للبحث عن الملفات التي سيتم نقلها إلى موقع FTP، ولكن تم إنشاء الملف يتم تشغيل الحدث قبل انتهاء العملية الأخرى من كتابة الملف.

سيكون الحل الأمثل هو فترة مهلة حتى لا يتعطل الخيط إلى الأبد قبل الاستسلام.

يحرر:بعد تجربة بعض الحلول أدناه، انتهى بي الأمر إلى تغيير ملف نظام بحيث كتب كافة الملفات ل Path.GetTempFileName(), ، ثم أجرى أ File.Move() إلى الموقع النهائي.حالما FileSystemWatcher تم إطلاق الحدث، وكان الملف مكتملًا بالفعل.

هل كانت مفيدة؟

المحلول

كان هذا هو الجواب الذي قدمته على سؤال ذو صلة:

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

نصائح أخرى

بدءًا من إجابة إريك، قمت بتضمين بعض التحسينات لجعل الكود أكثر إحكاما وقابل لإعادة الاستخدام.نأمل أن يكون مفيدا.

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

فيما يلي رمز عام للقيام بذلك، بشكل مستقل عن عملية الملف نفسها.وهذا مثال على كيفية استخدامه:

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

أو

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

يمكنك أيضًا تحديد عدد مرات إعادة المحاولة ووقت الانتظار بين مرات إعادة المحاولة.

ملحوظة:لسوء الحظ، لم يتم الكشف عن خطأ Win32 الأساسي (ERROR_SHARING_VIOLATION) مع .NET، لذلك أضفت وظيفة اختراق صغيرة (IsSharingViolation) بناءً على آليات التفكير للتحقق من ذلك.

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

لقد جمعت فصلًا مساعدًا لهذه الأنواع من الأشياء.سيعمل إذا كان لديك السيطرة على كل ما يمكنه الوصول إلى الملف.إذا كنت تتوقع خلافًا من مجموعة من الأشياء الأخرى، فهذا لا قيمة له على الإطلاق.

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
}

إنه يعمل باستخدام كائن المزامنة المسمى.يحاول أولئك الذين يرغبون في الوصول إلى الملف التحكم في كائن المزامنة المسمى، والذي يشارك اسم الملف (مع تحويل '\'s إلى '/'s).يمكنك إما استخدام Open()، والذي سيتوقف حتى يمكن الوصول إلى كائن المزامنة أو يمكنك استخدام TryOpen(TimeSpan)، الذي يحاول الحصول على كائن المزامنة للمدة المحددة ويعيد خطأ إذا لم يتمكن من الحصول عليه خلال الفترة الزمنية.يجب استخدام هذا على الأرجح داخل كتلة الاستخدام، لضمان تحرير الأقفال بشكل صحيح، وسيتم التخلص من التدفق (إذا كان مفتوحًا) بشكل صحيح عند التخلص من هذا الكائن.

لقد أجريت اختبارًا سريعًا باستخدام حوالي 20 شيئًا للقيام بعمليات قراءة/كتابة مختلفة للملف ولم أر أي تلف.من الواضح أنها ليست متقدمة جدًا، ولكن يجب أن تعمل مع غالبية الحالات البسيطة.

بالنسبة لهذا التطبيق بالذات، فإن مراقبة الملف مباشرة ستؤدي حتمًا إلى خطأ يصعب تتبعه، خاصة عندما يزيد حجم الملف.فيما يلي استراتيجيتان مختلفتان ستعملان.

  • بروتوكول نقل الملفات ملفين ولكن مشاهدة واحد فقط.على سبيل المثال، أرسل الملفات important.txt و important.finish.شاهد فقط ملف النهاية ولكن قم بمعالجة ملف txt.
  • FTP ملف واحد ولكن قم بإعادة تسميته عند الانتهاء.على سبيل المثال، أرسل important.wait واطلب من المرسل إعادة تسميته إلى important.txt عند الانتهاء.

حظ سعيد!

إحدى التقنيات التي استخدمتها منذ بعض الوقت كانت كتابة وظيفتي الخاصة.يمكنك التقاط الاستثناء بشكل أساسي وإعادة المحاولة باستخدام مؤقت يمكنك تشغيله لمدة محددة.إذا كان هناك طريقة أفضل، يرجى مشاركتها.

من MSDN:

يتم رفع الحدث الذي تم إنشاؤه بمجرد إنشاء ملف.إذا تم نسخ ملف أو نقله إلى دليل مراقب ، فسيتم رفع الحدث الذي تم إنشاؤه على الفور ، يليه حدث أو أكثر من الأحداث.

يمكن تعديل FileSystemWatcher الخاص بك بحيث لا يقوم بالقراءة/إعادة التسمية أثناء الحدث "OnCreated"، بل:

  1. يمتد مؤشر ترابط يستقصي حالة الملف حتى لا يتم قفله (باستخدام كائن FileInfo)
  2. يتم الاتصال مرة أخرى بالخدمة لمعالجة الملف بمجرد تحديد أن الملف لم يعد مقفلاً وجاهزًا للاستخدام

في معظم الحالات، سيعمل النهج البسيط مثل @harpo المقترح.يمكنك تطوير تعليمات برمجية أكثر تعقيدًا باستخدام هذا الأسلوب:

  • ابحث عن كافة المقابض المفتوحة للملف المحدد باستخدام SystemHandleInformation\SystemProcessInformation
  • فئة فرعية WaitHandle للوصول إلى المقبض الداخلي الخاص بها
  • قم بتمرير المقابض التي تم العثور عليها والمغلفة في WaitHandle ذات الفئة الفرعية إلى أسلوب WaitHandle.WaitAny

AD لنقل عملية تشغيل ملف samenameasstrasferedfile.trg الذي تم إنشاؤه بعد اكتمال نقل الملف.

ثم قم بإعداد FileSystemWatcher الذي سيطلق الحدث فقط على ملف *.trg.

لا أعرف ما الذي تستخدمه لتحديد حالة قفل الملف، ولكن شيئًا كهذا يجب أن يفعل ذلك.

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

        // check whether it's a lock problem

        Thread.Sleep( 100 );
    }
}

قد يكون الحل المحتمل هو الجمع بين مراقب نظام الملفات وبعض الاستقصاءات،

يتم إخطاره بكل تغيير في ملف ، وعند الإخطار بالتحقق مما إذا كان مغلقًا كما هو مذكور في الإجابة المقبولة حاليًا: https://stackoverflow.com/a/50800/6754146يتم نسخ رمز فتح تدفق الملفات من الإجابة وتعديله قليلاً:

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

بهذه الطريقة يمكنك التحقق من وجود ملف مقفل والحصول على إشعار عند إغلاقه خلال رد الاتصال المحدد، وبهذه الطريقة يمكنك تجنب الاستقصاء المفرط ولا تقوم بالعمل إلا عندما يكون مغلقًا بالفعل

أفعل ذلك بنفس طريقة جولزار، فقط استمر في المحاولة باستخدام حلقة.

في الحقيقة أنا لا أهتم حتى بمراقب نظام الملفات.يعد فحص محرك أقراص الشبكة بحثًا عن ملفات جديدة مرة واحدة في الدقيقة أمرًا رخيصًا.

ببساطة استخدم تغير الحدث مع NotifyFilter NotifyFilters.LastWrite:

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

لقد واجهت مشكلة مماثلة عند إضافة مرفق Outlook."استخدام" أنقذ اليوم.

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

ماذا عن هذا كخيار:

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

بالطبع إذا تم تخصيص الملف مسبقًا عند الإنشاء، فستحصل على نتيجة إيجابية خاطئة.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top