Espere até que o arquivo seja desbloqueado no .NET

103

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 um WaitOnFile () em algum lugar do .NET Framework?

Tenho um serviço que usa um FileSystemWatcher para procurar arquivos que devem ser transmitidos a um site FTP, mas o evento de arquivo criado dispara 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 ficasse para sempre antes de desistir.

Edit: Depois de experimentar algumas das soluções abaixo, acabei mudando o sistema para que todos os arquivos fossem gravados e Path.GetTempFileName(), em seguida, executei um File.Move()até o local final. Assim que o FileSystemWatcherevento foi disparado, o arquivo já estava completo.

Chris Wenham
fonte
4
Desde o lançamento do .NET 4.0, há uma maneira melhor de resolver esse problema?
jason

Respostas:

40

Esta foi a resposta que dei a uma 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;
    }
Eric Z Beard
fonte
8
Acho isso feio, mas a única solução possível
knoopx
6
Isso realmente vai funcionar no caso geral? se você abrir o arquivo em uma cláusula using (), o arquivo será fechado e desbloqueado quando o escopo using terminar. Se houver um segundo processo usando a mesma estratégia que este (repetir repetidamente), então, após a saída de WaitForFile (), haverá uma condição de corrida para saber se o arquivo será aberto ou não. Não?
Cheeso
75
Péssima ideia! Embora o conceito esteja certo, a melhor solução será retornar o FileStream em vez de um bool. Se o arquivo for bloqueado novamente antes que o usuário tenha a chance de obter o bloqueio do arquivo - ele obterá uma exceção mesmo se a função retornar "false"
Nissim
2
onde está o método do Fero?
Vbp
1
O comentário de Nissim é exatamente o que eu estava pensando também, mas se você for usar essa busca, não se esqueça de redefini-la para 0 após ler o byte. fs.Seek (0, SeekOrigin.Begin);
WHol
73

Começando com a resposta de 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;
}
mafu
fonte
16
Vim do futuro para dizer que esse código ainda funciona perfeitamente. Obrigado.
OnoSendai
6
@PabloCosta Exatamente! Não pode fechá-lo, porque se o fizesse, outro fio poderia entrar e abri-lo, anulando o propósito. Esta implementação está correta porque o mantém aberto! Deixe o chamador se preocupar com isso, é seguro usingem um nulo, apenas verifique se há nulo dentro do usingbloco.
doug65536,
2
"FileStream fs = null;" deve ser declarado fora do try, mas dentro do for. Em seguida, atribua e use fs dentro do try. O bloco catch deve fazer "if (fs! = Null) fs.Dispose ();" (ou apenas fs? .Dispose () em C # 6) para garantir que o FileStream que não está sendo retornado seja limpo corretamente.
Bill Menees,
1
É realmente necessário ler um byte? Na minha experiência, se você abriu o arquivo para acesso de leitura, você o tem, não precisa testar. Embora com o design aqui você não esteja forçando o acesso exclusivo, então é até possível que você possa ler o primeiro byte, mas nenhum outro (bloqueio de nível de byte). A partir da pergunta original, é provável que você abra com o nível de compartilhamento somente leitura, de modo que nenhum outro processo pode bloquear ou modificar o arquivo. De qualquer forma, acho que fs.ReadByte () é um desperdício completo ou não é o suficiente, dependendo do uso.
eselk
8
Usuário que circunstância fsnão pode ser nula no catchbloco? Se o FileStreamconstrutor lançar, a variável não receberá um valor e não há mais nada dentro de tryque possa lançar um IOException. Para mim, parece que não há problema em apenas fazer return new FileStream(...).
Matti Virkkunen
18

Aqui está um código genérico para fazer isso, independente da própria operação de 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 novas tentativas.

NOTA: Infelizmente, o erro Win32 subjacente (ERROR_SHARING_VIOLATION) não é exposto com .NET, então adicionei uma pequena função de hack ( IsSharingViolation) baseada 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;
    }
Simon Mourier
fonte
5
Eles realmente poderiam ter fornecido um SharingViolationException. Na verdade, eles ainda podem, de forma compatível com versões anteriores, contanto que desça de IOException. E eles realmente deveriam.
Roman Starkov
6
Marshal.GetHRForException msdn.microsoft.com/en-us/library/…
Steven T. Cramer
9
No .NET Framework 4.5, .NET Standard e .NET Core, HResult é uma propriedade pública na classe Exception. A reflexão não é mais necessária para isso. Do 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

Eu organizei uma aula de auxiliar para esse tipo de coisa. Isso funcionará se você tiver controle sobre tudo o que acessa o arquivo. Se você está esperando contenção de um monte de outras coisas, então isso não vale a pena.

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
}

Ele 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 o '\' s transformado em '/' s). Você pode usar Open (), que irá travar até que o mutex esteja acessível ou você pode usar TryOpen (TimeSpan), que tenta adquirir o mutex para a duração fornecida e retorna false se não puder adquirir dentro do intervalo de tempo. Provavelmente, isso deve ser usado dentro de um bloco em uso, para garantir que os bloqueios sejam liberados corretamente e que o fluxo (se aberto) seja descartado corretamente quando este objeto for descartado.

Eu fiz um teste rápido com ~ 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 para a maioria dos casos simples.

user152791
fonte
5

Para este aplicativo específico, observar diretamente o arquivo levará inevitavelmente 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 assistir 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 peça ao remetente que renomeie para important.txt quando terminar.

Boa sorte!

jason saldo
fonte
Isso é o oposto de automático. É como obter o arquivo manualmente, com mais etapas.
HackSlash de
4

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

Gulzar Nazim
fonte
3

Do MSDN :

O evento OnCreated é gerado assim que um arquivo é criado. Se um arquivo estiver sendo copiado ou transferido para um diretório monitorado, o evento OnCreated será gerado imediatamente, seguido por um ou mais eventos OnChanged.

Seu FileSystemWatcher poderia ser modificado para não fazer sua leitura / renomeação durante o evento "OnCreated", mas sim:

  1. Estende um thread que pesquisa o status do arquivo até que ele não seja bloqueado (usando um objeto FileInfo)
  2. Chama de volta para o serviço para processar o arquivo assim que determina que o arquivo não está mais bloqueado e está pronto para ser usado
Guy Starbuck
fonte
1
Gerar o thread do observador do sistema de arquivos pode fazer com que o buffer subjacente transborde, perdendo muitos arquivos alterados. Uma abordagem melhor será criar uma fila de consumidor / produtor.
Nissim
2

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

  • Encontre todos os identificadores abertos para o arquivo selecionado usando SystemHandleInformation \ SystemProcessInformation
  • Classe WaitHandle da subclasse para obter acesso ao seu identificador interno
  • Passe identificadores encontrados agrupados no método WaitHandle com subclasse para o método WaitHandle.WaitAny
aku
fonte
2

Anúncio para transferir o arquivo de gatilho do processo SameNameASTrasferedFile.trg que é criado após a conclusão da transmissão do arquivo.

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

Rudi
fonte
1

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

enquanto (verdadeiro)
{
    experimentar {
        stream = File.Open (fileName, fileMode);
        pausa;
    }
    catch (FileIOException) {

        // verifique se é um problema de bloqueio

        Thread.Sleep (100);
    }
}
arpão
fonte
1
Um pouco tarde, mas quando o arquivo estiver bloqueado de alguma forma, você nunca sairá do loop. Você deve adicionar um contador (ver 1ª resposta).
Peter
0

Uma possível solução seria, combinar um observador de sistema de arquivos com algumas pesquisas,

seja notificado para cada alteração em um arquivo e, ao ser notificado, verifique se ele está bloqueado conforme declarado na resposta atualmente aceita: https://stackoverflow.com/a/50800/6754146 O código para abrir o fluxo de arquivos é 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 for fechado durante o retorno de chamada especificado, desta forma você evita a pesquisa excessivamente agressiva e só faz o trabalho quando ele pode estar realmente fechado

Florian K
fonte
-1

Eu faço isso da mesma forma que Gulzar, apenas continue 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.

Jonathan Allen
fonte
2
Pode ser barato, mas uma vez por minuto é muito tempo para muitos aplicativos. O monitoramento em tempo real é essencial às vezes. Em vez de implementar algo que ouvirá as mensagens do sistema de arquivos em C # (que não é a linguagem mais conveniente para essas coisas), você usa o FSW.
ThunderGr
-1

Basta usar o evento Changed 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;
Bernhard Hochgatterer
fonte
1
FileSystemWatcher não notifica apenas quando um arquivo é gravado. Freqüentemente, ele irá notificá-lo várias vezes para uma "única" gravação lógica e, se você tentar abrir o arquivo após receber a primeira notificação, obterá uma exceção.
Ross
-1

Eu tive um problema semelhante ao adicionar um anexo do Outlook. "Usando" 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);
Jahmal23
fonte
-3

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, se o tamanho do arquivo for pré-alocado na criação, você obterá um falso positivo.

Ralph Shillington
fonte
1
Se o processo de gravação no arquivo for pausado por mais de um segundo ou armazenado na memória por mais de um segundo, você receberá outro falso positivo. Não creio que seja uma boa solução em nenhuma circunstância.
Chris Wenham