Pregunta

¿Cuál es la forma más sencilla de bloquear un hilo hasta que un archivo se haya desbloqueado y sea accesible para leerlo y cambiarle el nombre?Por ejemplo, ¿hay WaitOnFile() en algún lugar de .NET Framework?

Tengo un servicio que utiliza FileSystemWatcher para buscar archivos que se transmitirán a un sitio FTP, pero el archivo creado El evento se activa antes de que el otro proceso haya terminado de escribir el archivo.

La solución ideal tendría un período de tiempo de espera para que el hilo no se cuelgue para siempre antes de darse por vencido.

Editar:Después de probar algunas de las soluciones siguientes, terminé cambiando el sistema para que todos los archivos escritos en Path.GetTempFileName(), luego realizó un File.Move() hasta la ubicación definitiva.Tan pronto como FileSystemWatcher evento activado, el archivo ya estaba completo.

¿Fue útil?

Solución

Esta fue la respuesta que di en un pregunta relacionada:

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

Otros consejos

A partir de la respuesta de Eric, incluí algunas mejoras para hacer el código mucho más compacto y reutilizable.Espero que sea útil.

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

Aquí hay un código genérico para hacer esto, independiente de la operación del archivo en sí.Este es un ejemplo de cómo usarlo:

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

o

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

También puede definir el recuento de reintentos y el tiempo de espera entre reintentos.

NOTA:Desafortunadamente, el error subyacente de Win32 (ERROR_SHARING_VIOLATION) no está expuesto con .NET, por lo que agregué una pequeña función de pirateo (IsSharingViolation) basado en mecanismos de reflexión para comprobar esto.

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

Organicé una clase de ayuda para este tipo de cosas.Funcionará si tienes control sobre todo lo que accedería al archivo.Si esperas competencia de muchas otras cosas, entonces esto es bastante inútil.

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
}

Funciona usando un mutex con nombre.Aquellos que deseen acceder al archivo intentan adquirir el control del mutex nombrado, que comparte el nombre del archivo (con los '\'s convertidos en '/'s).Puede usar Open(), que se detendrá hasta que se pueda acceder al mutex o puede usar TryOpen(TimeSpan), que intenta adquirir el mutex durante la duración dada y devuelve falso si no puede adquirirlo dentro del lapso de tiempo.Lo más probable es que esto deba usarse dentro de un bloque de uso, para garantizar que los bloqueos se liberen correctamente y que la secuencia (si está abierta) se elimine correctamente cuando se elimine este objeto.

Hice una prueba rápida con ~20 cosas para realizar varias lecturas/escrituras del archivo y no vi corrupción.Obviamente no es muy avanzado, pero debería funcionar para la mayoría de casos simples.

Para esta aplicación en particular, observar directamente el archivo conducirá inevitablemente a un error difícil de rastrear, especialmente cuando aumenta el tamaño del archivo.Aquí hay dos estrategias diferentes que funcionarán.

  • Ftp dos archivos pero solo mira uno.Por ejemplo envíe los archivos importante.txt y importante.finish.Solo mire el archivo final pero procese el txt.
  • Envíe por FTP un archivo, pero cámbiele el nombre cuando haya terminado.Por ejemplo, envíe importante.wait y haga que el remitente le cambie el nombre a importante.txt cuando haya terminado.

¡Buena suerte!

Una de las técnicas que utilicé hace algún tiempo fue escribir mi propia función.Básicamente, detecta la excepción y vuelve a intentarlo usando un temporizador que puedes activar durante un período específico.Si hay una manera mejor, compártala.

De MSDN:

El evento increado se plantea tan pronto como se crea un archivo.Si un archivo se copia o se transfiere a un directorio observado, el evento incriminado se planteará inmediatamente, seguido de uno o más eventos en el cambio.

Su FileSystemWatcher podría modificarse para que no realice su lectura/cambio de nombre durante el evento "OnCreated", sino más bien:

  1. Genera un hilo que sondea el estado del archivo hasta que no esté bloqueado (usando un objeto FileInfo)
  2. Vuelve a llamar al servicio para procesar el archivo tan pronto como determina que el archivo ya no está bloqueado y está listo para funcionar.

En la mayoría de los casos, un enfoque simple como el que sugirió @harpo funcionará.Puede desarrollar código más sofisticado utilizando este enfoque:

  • Encuentre todos los identificadores abiertos para el archivo seleccionado usando SystemHandleInformation\SystemProcessInformation
  • Subclase clase WaitHandle para obtener acceso a su identificador interno
  • Pase los identificadores encontrados envueltos en WaitHandle subclasificado al método WaitHandle.WaitAny

AD para transferir el archivo de activación Archivo samenameStrasferedFile.trg que se crea después de que se completa la transmisión de archivos.

Luego configure FileSystemWatcher que activará el evento solo en el archivo *.trg.

No sé qué estás usando para determinar el estado de bloqueo del archivo, pero algo como esto debería bastar.

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

        // check whether it's a lock problem

        Thread.Sleep( 100 );
    }
}

Una posible solución sería combinar un observador del sistema de archivos con algunas encuestas,

Reciba notificaciones para cada cambio en un archivo, y cuando se le notifica, verifique si se bloquea como se indica en la respuesta aceptada actualmente: https://stackoverflow.com/a/50800/6754146El código para abrir el flujo de archivos se copia de la respuesta y se modifica ligeramente:

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

De esta manera, puede verificar si un archivo está bloqueado y recibir una notificación cuando se cierre durante la devolución de llamada especificada, de esta manera evita el sondeo demasiado agresivo y solo hace el trabajo cuando realmente puede estar cerrado.

Lo hago de la misma manera que Gulzar, sólo sigo intentándolo con un bucle.

De hecho, ni siquiera me molesto con el observador del sistema de archivos.Sondear una unidad de red en busca de archivos nuevos una vez por minuto es económico.

Simplemente use el Cambió evento con NotifyFilter Notificarfiltros.Últimaescritura:

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

Me encontré con un problema similar al agregar un archivo adjunto de Outlook."Usar" salvó el día.

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

¿Qué tal esto como opció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);
    }
}

Por supuesto, si el tamaño del archivo está preasignado en la creación, obtendría un falso positivo.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top