Espere hasta que el archivo se desbloquee en .NET

103

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

Tengo un servicio que usa FileSystemWatcher para buscar archivos que se van a transmitir a un sitio FTP, pero el evento creado por el archivo 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 cuelgue para siempre antes de darse por vencido.

Editar: Después de probar algunas de las soluciones a continuación, terminé cambiando el sistema para que todos los archivos se escribieran y Path.GetTempFileName()luego realicé una File.Move()en la ubicación final. Tan pronto como FileSystemWatcherse disparó el evento, el archivo ya estaba completo.

Chris Wenham
fuente
4
Desde el lanzamiento de .NET 4.0, ¿existe una mejor manera de resolver este problema?
Jason

Respuestas:

40

Esta fue la respuesta que di a una 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;
    }
Eric Z Barba
fuente
8
Encuentro esto feo pero la única solución posible
Knoopx
6
¿Esto realmente va a funcionar en el caso general? si abre el archivo en una cláusula using (), el archivo se cierra y se desbloquea cuando finaliza el alcance del uso. Si hay un segundo proceso que usa la misma estrategia que este (reintentar repetidamente), luego de salir de WaitForFile (), hay una condición de carrera con respecto a si el archivo se podrá abrir o no. ¿No?
Cheeso
75
¡Mala idea! Si bien el concepto es correcto, una mejor solución será devolver FileStream en lugar de un bool. Si el archivo se vuelve a bloquear antes de que el usuario tenga la oportunidad de obtener su bloqueo en el archivo, obtendrá una excepción incluso si la función devolvió "falso"
Nissim
2
¿Dónde está el método de Fero?
Vbp
1
El comentario de Nissim es exactamente lo que estaba pensando también, pero si vas a usar esa búsqueda, no olvides restablecerla a 0 después de leer el byte. fs.Seek (0, SeekOrigin.Begin);
WHol
73

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

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
fuente
16
Vengo del futuro para decir que este código todavía funciona de maravilla. Gracias.
OnoSendai
6
@PabloCosta ¡Exacto! No puede cerrarlo, porque si lo hiciera, otro hilo podría entrar y abrirlo, frustrando el propósito. ¡Esta implementación es correcta porque la mantiene abierta! Deje que la persona que llama se preocupe por eso, es seguro hacerlo usingen un nulo, solo verifique si hay nulo dentro del usingbloque.
doug65536
2
"FileStream fs = null;" debe declararse fuera del try pero dentro del for. Luego asigne y use fs dentro del try. El bloque de captura debería hacer "if (fs! = Null) fs.Dispose ();" (¿o simplemente fs? .Dispose () en C # 6) para garantizar que el FileStream que no se devuelve se limpie correctamente.
Bill Menees
1
¿Es realmente necesario leer un byte? En mi experiencia, si ha abierto el archivo para acceso de lectura, lo tiene, no tiene que probarlo. Aunque con el diseño aquí no está forzando el acceso exclusivo, por lo que es posible que pueda leer el primer byte, pero no otros (bloqueo de nivel de byte). A partir de la pregunta original, es probable que abra con un nivel de recurso compartido de solo lectura, por lo que ningún otro proceso puede bloquear o modificar el archivo. En cualquier caso, creo que fs.ReadByte () es un desperdicio completo o no es suficiente, según el uso.
eselk
8
Usuario ¿qué circunstancia no puede fsser nula en el catchbloque? Si el FileStreamconstructor arroja, no se le asignará un valor a la variable, y no hay nada más dentro tryque pueda arrojar un IOException. A mí me parece que debería estar bien hacerlo return new FileStream(...).
Matti Virkkunen
18

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 número 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) basada en mecanismos de reflexión para verificar 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;
    }
Simón Mourier
fuente
5
Realmente podrían haber proporcionado un SharingViolationException. De hecho, todavía pueden, de manera compatible con versiones anteriores, siempre que descienda de IOException. Y realmente deberían hacerlo.
Roman Starkov
6
Marshal.GetHRForException msdn.microsoft.com/en-us/library/…
Steven T. Cramer
9
En .NET Framework 4.5, .NET Standard y .NET Core, HResult es una propiedad pública en la clase Exception. La reflexión ya no es necesaria para esto. Desde 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

Organice una clase de ayuda para este tipo de cosas. Funcionará si tiene control sobre todo lo que accedería al archivo. Si espera una contención 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 las '\' convertidas 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 determinada y devuelve falso si no puede adquirirlo dentro del intervalo de tiempo. Lo más probable es que esto se use dentro de un bloque de uso, para garantizar que las cerraduras se liberen correctamente y que la corriente (si está abierta) se eliminará correctamente cuando se elimine este objeto.

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

usuario152791
fuente
5

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 important.txt e important.finish. Solo observe el archivo de finalización pero procese el txt.
  • FTP un archivo pero cámbiele el nombre cuando termine. Por ejemplo, envíe important.wait y haga que el remitente le cambie el nombre a important.txt cuando termine.

¡Buena suerte!

jason saldo
fuente
Eso es lo contrario de automático. Eso es como obtener el archivo manualmente, con más pasos.
HackSlash
4

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 con un temporizador que puedes disparar durante una duración determinada. Si hay una mejor manera, por favor compártala.

Gulzar Nazim
fuente
3

Desde MSDN :

El evento OnCreated se genera tan pronto como se crea un archivo. Si un archivo se está copiando o transfiriendo a un directorio supervisado, el evento OnCreated se generará inmediatamente, seguido de uno o más eventos OnChanged.

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

  1. Distribuye 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
Guy Starbuck
fuente
1
Generar el hilo del vigilante del sistema de archivos puede hacer que el búfer subyacente se desborde, por lo que se pierden muchos archivos modificados. Un mejor enfoque será crear una cola de consumidores / productores.
Nissim
2

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

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

Anuncio para transferir el archivo de activación del proceso SameNameASTrasferedFile.trg que se crea después de que se completa la transmisión del archivo.

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

Rudi
fuente
1

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

mientras (cierto)
{
    tratar {
        stream = Archivo.Open (nombre de archivo, modo de archivo);
        descanso;
    }
    catch (FileIOException) {

        // verifica si es un problema de bloqueo

        Thread.Sleep (100);
    }
}
arpón
fuente
1
Un poco tarde, pero cuando el archivo está bloqueado de alguna manera, nunca saldrá del bucle. Debe agregar un contador (ver la primera respuesta).
Peter
0

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

reciba una notificación por cada cambio en un archivo y, al recibir una notificación, verifique si está bloqueado como se indica en la respuesta actualmente aceptada: https://stackoverflow.com/a/50800/6754146 El código para abrir el flujo de archivos se copia de la respuesta y ligeramente modificado:

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 un archivo si está bloqueado y recibir una notificación cuando se cierre sobre la devolución de llamada especificada, de esta manera evita el sondeo demasiado agresivo y solo hace el trabajo cuando puede estar realmente cerrado

Florian K
fuente
-1

Lo hago de la misma manera que Gulzar, solo sigue intentándolo con un bucle.

De hecho, ni siquiera me preocupo por el observador del sistema de archivos. Sondear una unidad de red en busca de archivos nuevos una vez por minuto es barato.

Jonathan Allen
fuente
2
Puede ser económico, pero una vez por minuto es demasiado largo para muchas aplicaciones. El monitoreo en tiempo real es esencial a veces. En lugar de tener que implementar algo que escuchará los mensajes del sistema de archivos en C # (no es el lenguaje más conveniente para estas cosas), usa FSW.
ThunderGr
-1

Simplemente use el evento Changed con 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
fuente
1
FileSystemWatcher no solo notifica cuando se termina de escribir un archivo. A menudo le notificará varias veces para una escritura lógica "única", y si intenta abrir el archivo después de recibir la primera notificación, obtendrá una excepción.
Ross
-1

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

¿Qué tal esto como una 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 archivo está preasignado en la creación, obtendría un falso positivo.

Ralph Shillington
fuente
1
Si el proceso de escritura en el archivo se detiene durante más de un segundo, o se almacena en la memoria durante más de un segundo, obtendrá otro falso positivo. No creo que esta sea una buena solución bajo ninguna circunstancia.
Chris Wenham