Aguarde até que o arquivo esteja desbloqueado no .NET

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

Eu tenho um serviço que usa um FileSystemWatcher para procurar arquivos que devem ser transmitidos para um site FTP, mas o evento de arquivo criado é acionado antes que o outro processo tenha terminado de gravar o arquivo.

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

Edit: Depois de experimentar algumas das soluções abaixo, acabei alterando o sistema para que todos os arquivos File.Move() e, em seguida, executei um File.Move() para o local final. Assim que o evento FileSystemWatcher foi triggersdo, o arquivo já estava completo.

Esta foi a resposta que dei sobre uma questão relacionada :

  ///  /// Blocks until the file is not locked any more. ///  ///  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; } 

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

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

OBSERVAÇÃO: Infelizmente, o erro subjacente do Win32 (ERROR_SHARING_VIOLATION) não é exposto com o .NET, portanto, adicionei uma pequena function de hack ( IsSharingViolation ) com base nos mecanismos de reflection para verificar isso.

  ///  /// Wraps sharing violations that could occur on a file IO operation. ///  /// The action to execute. May not be null. public static void WrapSharingViolations(WrapSharingViolationsCallback action) { WrapSharingViolations(action, null, 10, 100); } ///  /// Wraps sharing violations that could occur on a file IO operation. ///  /// The action to execute. May not be null. /// The exceptions callback. May be null. /// The retry count. /// The wait time in milliseconds. 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; } } } } ///  /// Defines a sharing violation wrapper delegate. ///  public delegate void WrapSharingViolationsCallback(); ///  /// Defines a sharing violation wrapper delegate for handling exception. ///  public delegate bool WrapSharingViolationsExceptionsCallback(IOException ioe, int retry, int retryCount, int waitTime); ///  /// Determines whether the specified exception is a sharing violation exception. ///  /// The exception. May not be null. ///  /// true if the specified exception is a sharing violation exception; otherwise, false. ///  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 } ///  /// Gets the HRESULT of the specified exception. ///  /// The exception to test. May not be null. /// The default value in case of an error. /// The HRESULT value. 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 joguei uma class de ajuda para esse tipo de coisa. Ele 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 é muito inútil.

 using System; using System.IO; using System.Threading; ///  /// 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). ///  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 ///  /// Opens the stream when it is not locked. If the file is locked, then ///  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 ‘\’ se transformou em ‘/’ s). 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 para a duração dada e retorna false se não puder adquirir dentro do intervalo de tempo. Isso provavelmente deve ser usado dentro de um bloco de uso, para garantir que os bloqueios sejam liberados corretamente, e o stream (se aberto) será descartado adequadamente quando esse object for descartado.

Eu fiz um teste rápido com ~ 20 coisas para fazer várias leituras / escritas do arquivo e não vi nenhuma corrupção. Obviamente, não é muito avançado, mas deve funcionar para a maioria dos casos simples.

Para este aplicativo em particular, observar diretamente o arquivo levará inevitavelmente a um erro difícil de rastrear, especialmente quando o tamanho do arquivo aumenta. Aqui estão duas estratégias diferentes que irão funcionar.

  • Ftp dois arquivos, mas só assisto um. Por exemplo, envie os arquivos important.txt e important.finish. Apenas observe o arquivo de acabamento, mas processe o texto.
  • FTP um arquivo, mas renomeá-lo quando terminar. Por exemplo, envie important.wait e peça ao remetente que renomeie para important.txt quando terminar.

Boa sorte!

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

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 events OnChanged.

Seu FileSystemWatcher pode ser modificado para não ler / renomear durante o evento “OnCreated”, mas sim:

  1. Spanws um tópico que pesquisa o status do arquivo até que ele não seja bloqueado (usando um object FileInfo)
  2. Retorna ao serviço para processar o arquivo assim que ele determina que o arquivo não está mais bloqueado e está pronto para ser usado

Eu não sei o que você está usando para determinar o status de bloqueio do arquivo, mas algo assim deveria fazer isso.

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

         // verifique se é um problema de bloqueio

         Thread.Sleep (100);
     }
 }

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

  • Localizar todas as alças abertas para o arquivo selecionado usando SystemHandleInformation \ SystemProcessInformation
  • Subclass WaitHandle class para obter access ao seu identificador interno
  • Passe as alças encontradas envolvidas no WaitHandle com a subclass no método WaitHandle.WaitAny

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

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

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

seja notificado para cada alteração em um arquivo e, ao ser notificado, verifique se está bloqueado, conforme indicado na resposta aceita no momento: https://stackoverflow.com/a/50800/6754146 O código para abrir o stream de arquivos é copiado da resposta e ligeiramente modificado:

 public static void CheckFileLock(string directory, string filename, Func 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 um arquivo se ele estiver bloqueado e ser notificado quando estiver fechado sobre o retorno de chamada especificado, dessa forma você evita o polling excessivamente agressivo e só faz o trabalho quando ele pode estar realmente fechado

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

Na verdade, nem me incomodo com o observador do sistema de arquivos. Polling uma unidade de rede para novos arquivos uma vez por minuto é barato.

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; 

Eu me deparei com 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); 

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.