Pergunta

Qual é a maneira mais simples de bloquear um thread até que um arquivo seja desbloqueado e esteja acessível para leitura e renomeação?Por exemplo, existe WaitOnFile() em algum lugar do .NET Framework?

Tenho um serviço que usa um FileSystemWatcher para procurar arquivos que serão transmitidos para um site FTP, mas o arquivo criado o evento é acionado antes que o outro processo termine de gravar o arquivo.

A solução ideal teria um período de tempo limite para que o thread não travasse para sempre antes de desistir.

Editar:Depois de experimentar algumas das soluções abaixo, acabei mudando o sistema para que todos os arquivos sejam gravados Path.GetTempFileName(), então realizou um File.Move() para o local final.Assim que o FileSystemWatcher evento disparado, o arquivo já estava completo.

Foi útil?

Solução

Esta foi a resposta que dei em um pergunta 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;
    }

Outras dicas

A partir da resposta do Eric, incluí algumas melhorias para tornar o código muito mais compacto e reutilizável.Espero que seja ú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;
}

Aqui está um código genérico para fazer isso, independente da própria operação do arquivo.Este é um exemplo de como usá-lo:

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

ou

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

Você também pode definir a contagem de novas tentativas e o tempo de espera entre as tentativas.

OBSERVAÇÃO:Infelizmente, o erro subjacente do Win32 (ERROR_SHARING_VIOLATION) não é exposto no .NET, então adicionei uma pequena função de hack (IsSharingViolation) com base em mecanismos de reflexão para verificar isso.

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

Eu criei uma classe auxiliar para esse tipo de coisa.Funcionará se você tiver controle sobre tudo que acessaria o arquivo.Se você está esperando contenção de um monte de outras coisas, então isso é 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 um mutex nomeado.Aqueles que desejam acessar o arquivo tentam adquirir o controle do mutex nomeado, que compartilha o nome do arquivo (com os '\' transformados em '/').Você pode usar Open(), que irá parar até que o mutex esteja acessível ou você pode usar TryOpen(TimeSpan), que tenta adquirir o mutex durante um determinado período e retorna falso se não puder adquirir dentro do intervalo de tempo.Provavelmente, isso deve ser usado dentro de um bloco using, para garantir que os bloqueios sejam liberados corretamente e que o fluxo (se aberto) seja descartado corretamente quando este objeto for descartado.

Fiz um teste rápido com cerca de 20 coisas para fazer várias leituras/gravações do arquivo e não vi corrupção.Obviamente não é muito avançado, mas deve funcionar na maioria dos casos simples.

Para esta aplicação específica, a observação direta do arquivo inevitavelmente levará a um bug difícil de rastrear, especialmente quando o tamanho do arquivo aumenta.Aqui estão duas estratégias diferentes que funcionarão.

  • FTP dois arquivos, mas assista apenas um.Por exemplo envie os arquivos important.txt e important.finish.Observe apenas o arquivo final, mas processe o txt.
  • FTP um arquivo, mas renomeie-o quando terminar.Por exemplo, envie important.wait e faça com que o remetente o renomeie para important.txt quando terminar.

Boa sorte!

Uma das técnicas que usei há algum tempo foi escrever minha própria função.Basicamente, capture a exceção e tente novamente usando um cronômetro que você pode disparar por um período especificado.Se houver uma maneira melhor, por favor, compartilhe.

De MSDN:

O evento OnCreated é aumentado assim que um arquivo for criado.Se um arquivo estiver sendo copiado ou transferido para um diretório assistido, o evento OnCreated será aumentado imediatamente, seguido por um ou mais eventos em conjunto.

Seu FileSystemWatcher pode ser modificado para que não seja lido/renomeado durante o evento "OnCreated", mas sim:

  1. Abrange um thread que pesquisa o status do arquivo até que ele não seja bloqueado (usando um objeto FileInfo)
  2. Chama de volta ao serviço para processar o arquivo assim que determinar que o arquivo não está mais bloqueado e está pronto para uso

Na maioria dos casos, uma abordagem simples como a sugerida por @harpo funcionará.Você pode desenvolver código mais sofisticado usando esta abordagem:

  • Encontre todos os identificadores abertos para o arquivo selecionado usando SystemHandleInformation\SystemProcessInformation
  • Subclasse da classe WaitHandle para obter acesso ao seu identificador interno
  • Passe identificadores encontrados agrupados na subclasse WaitHandle para o método WaitHandle.WaitAny

Anúncio para transferir o processo de acionamento do processo samenameastraferedfile.trg criado após a conclusão da transmissão do arquivo.

Em seguida, configure o FileSystemWatcher que disparará o evento apenas no arquivo *.trg.

Não sei o que você está usando para determinar o status de bloqueio do arquivo, mas algo assim deve resolver.

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

        // check whether it's a lock problem

        Thread.Sleep( 100 );
    }
}

Uma possível solução seria combinar um filesystemwatcher com alguma pesquisa,

Seja notificado para todas as alterações em um arquivo e, ao ser notificado, cheque se ele está bloqueado conforme declarado na resposta atualmente aceita: https://stackoverflow.com/a/50800/6754146O código para abrir o filestream é copiado da resposta e ligeiramente 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);
}

Desta forma você pode verificar se um arquivo está bloqueado e ser notificado quando ele for fechado no retorno de chamada especificado, dessa forma você evita a pesquisa excessivamente agressiva e só faz o trabalho quando ele pode estar realmente fechado

Eu faço da mesma forma que Gulzar, apenas continuo tentando com um loop.

Na verdade, nem me preocupo com o observador do sistema de arquivos.Pesquisar novos arquivos em uma unidade de rede uma vez por minuto é barato.

Basta usar o Mudado evento com o NotifyFilter NotifyFilters.LastWrite:

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

Encontrei um problema semelhante ao adicionar um anexo do Outlook.“Usar” salvou o dia.

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

Que tal isso como uma opção:

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

É claro que se o tamanho do arquivo for pré-alocado na criação, você obterá um falso positivo.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top