Espere hasta que el archivo esté desbloqueado in.NET


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

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

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

Author: Peter Mortensen, 2008-09-09

15 answers

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;
    }
 36
Author: Eric Z Beard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:54:31

A partir de la respuesta de Eric, he incluido 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;
}
 55
Author: mafu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-06 18:39:48

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 Win32 subyacente (ERROR_SHARING_VIOLATION) no está expuesto con.NET, por lo que he agregado una pequeña función de hackeo (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;
    }
 15
Author: Simon Mourier,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-01-16 18:06:31

Organicé una clase de ayuda para este tipo de cosas. Funcionará si tienes control sobre todo lo que accedería al archivo. Si usted está esperando la contención de un montón de 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 llamado. Aquellos que desean 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 el mutex sea accesible o puede usar TryOpen (TimeSpan), que intenta adquirir el mutex durante la duración dada y devuelve false si no puede adquirir dentro del lapso de tiempo. Lo más probable es que esto se use dentro de un bloque de uso, para asegurarse de que los bloqueos se liberen correctamente, y la secuencia (si está abierta) se eliminará correctamente cuando se elimine este objeto.

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

 13
Author: user152791,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-08-07 22:40:56

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

  • Ftp dos archivos, pero solo ver uno. Por ejemplo, envíe los archivos importantes.txt e importante.terminar. Solo observe el archivo de finalización, pero procese el txt.
  • FTP un archivo, pero cambiar el nombre cuando se hace. Por ejemplo, enviar importante.espere y haga que el remitente lo cambie a importante.txt cuando termine.

¡Buena suerte!

 5
Author: jason saldo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-09 00:20:33

Una de las técnicas que utilicé hace algún tiempo fue escribir mi propia función. Básicamente captura la excepción y vuelve a intentarlo usando un temporizador que puedes disparar durante un tiempo especificado. Si hay una manera mejor, por favor comparta.

 4
Author: Gulzar Nazim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 21:37:55

De MSDN :

El evento onCreated se eleva tan pronto como se crea un archivo. Si un archivo es copiado o transferido a un directorio visto, el evento onCreated se levantará inmediatamente, seguido por uno o más eventos onChanged.

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

  1. Spanws 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
 3
Author: Guy Starbuck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 21:48:11

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

  • Encuentre todos los manejadores abiertos para el archivo seleccionado usando SystemHandleInformation \ SystemProcessInformation
  • Subclase WaitHandle class para obtener acceso a su manejador interno
  • Pasa los mangos encontrados envueltos en una subclase WaitHandle a WaitHandle.Método WaitAny
 2
Author: aku,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-09 00:11:30

Ad para transferir el archivo disparador del proceso SameNameASTrasferedFile.trg que se crea después de la transmisión de archivos se ha completado.

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

 2
Author: Rudi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-08 15:10:18

No se lo que está usando para determinar el estado de bloqueo del archivo, pero algo como esto debería hacerlo.

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

        // check whether it's a lock problem

        Thread.Sleep( 100 );
    }
}
 1
Author: harpo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 21:42:27

Una posible solución sería, para combinar un filesystemwatcher con algún sondeo,

Reciba una notificación por cada Cambio en un Archivo, y cuando reciba una notificación, verifique si bloqueado como se indica en la respuesta actualmente aceptada: https://stackoverflow.com/a/50800/6754146 El código para abrir el filestream 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 Comprobar si un archivo está bloqueado y recibir una notificación cuando se cierra sobre el especificado devolución de llamada, de esta manera se evita el sondeo demasiado agresivo y solo hacer el trabajo cuando puede ser en realidad ser cerrado

 0
Author: Florian K,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:46:56

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

De hecho ni siquiera me molesto con el file system watcher. Sondear una unidad de red para nuevos archivos una vez por minuto es barato.

 -1
Author: Jonathan Allen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 23:14:58

Simplemente use el evento Changed con el NotifyFilter NotifyFilters.LastWrite:

var watcher = new FileSystemWatcher {
      Path = @"c:\temp\test",
      Filter = "*.xml",
      NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += watcher_Changed; 
watcher.EnableRaisingEvents = true;
 -1
Author: Bernhard Hochgatterer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-01-22 21:09:59

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);
 -1
Author: Jahmal23,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-04 16:58:04

¿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 tamaño del archivo está preasignado en el create, obtendrás un falso positivo.

 -3
Author: Ralph Shillington,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-07-23 12:56:35