Подождите, пока файл не будет разблокирован в .NET
Вопрос
Каков самый простой способ заблокировать поток до тех пор, пока файл не будет разблокирован и доступен для чтения и переименования?Например, есть ли WaitOnFile() где-нибудь в .NET Framework?
У меня есть служба, которая использует FileSystemWatcher для поиска файлов, которые должны быть переданы на FTP-сайт, но созданный файл событие срабатывает до того, как другой процесс завершит запись файла.
Идеальным решением было бы установить тайм-аут, чтобы поток не зависал вечно, прежде чем сдаться.
Редактировать:Опробовав некоторые из приведенных ниже решений, я в итоге изменил система чтобы все файлы записывались в Path.GetTempFileName()
, затем выполнил File.Move()
к конечному местоположению.Как только FileSystemWatcher
событие сработало, файл уже был завершен.
Решение
Это был ответ, который я дал на связанный с этим вопрос:
/// <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;
}
Другие советы
Отталкиваясь от ответа Эрика, я включил некоторые улучшения, чтобы сделать код намного более компактным и многоразовым.Надеюсь, это полезно.
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;
}
Вот общий код для этого, независимый от самой файловой операции.Это пример того, как его использовать:
WrapSharingViolations(() => File.Delete(myFile));
или
WrapSharingViolations(() => File.Copy(mySourceFile, myDestFile));
Вы также можете определить количество повторных попыток и время ожидания между повторными попытками.
ПРИМЕЧАНИЕ:К сожалению, основная ошибка Win32 (ERROR_SHARING_VIOLATION) не отображается в .NET, поэтому я добавил небольшую функцию взлома (IsSharingViolation
) на основе механизмов отражения для проверки этого.
/// <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;
}
Я собрал вспомогательный класс для подобных вещей.Это сработает, если у вас есть контроль над всем, что могло бы получить доступ к файлу.Если вы ожидаете разногласий от кучи других вещей, то это довольно бесполезно.
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
}
Это работает с использованием именованного мьютекса.Желающие получить доступ к файлу пытаются получить контроль над именованным мьютексом, который разделяет имя файла (с '\'s, превращенным в '/'s).Вы можете либо использовать Open(), который будет останавливаться до тех пор, пока мьютекс не станет доступным, либо вы можете использовать TryOpen(TimeSpan), который пытается получить мьютекс в течение заданного периода времени и возвращает false, если он не может получить в течение этого периода времени.Скорее всего, это следует использовать внутри блока using, чтобы гарантировать, что блокировки разблокированы должным образом, и поток (если он открыт) будет правильно удален при удалении этого объекта.
Я провел быстрый тест с ~ 20 функциями для выполнения различных операций чтения / записи файла и не увидел никаких повреждений.Очевидно, что это не очень продвинутый метод, но он должен работать в большинстве простых случаев.
Для этого конкретного приложения непосредственное наблюдение за файлом неизбежно приведет к трудноотслеживаемой ошибке, особенно при увеличении размера файла.Вот две разные стратегии, которые будут работать.
- Передайте по Ftp два файла, но смотрите только один.Например, отправьте файлы important.txt и important.finish.Следите только за готовым файлом, но обрабатывайте текстовый текст.
- Передайте по FTP один файл, но переименуйте его, когда закончите.Например, отправить важное.подождите и попросите отправителя переименовать его в important.txt когда закончите.
Удачи вам!
Одним из методов, который я использовал некоторое время назад, было написание моей собственной функции.В основном перехватывайте исключение и повторяйте попытку, используя таймер, который вы можете запускать в течение заданного времени.Если есть способ получше, пожалуйста, поделитесь.
От MSDN:
Событие onCreated вызывается, как только создается файл.Если файл копируется или переносится в просматриваемый каталог, немедленно будет вызвано событие onCreated , за которым последует одно или несколько событий OnChanged.
Ваш FileSystemWatcher может быть изменен таким образом, чтобы он не выполнял чтение / переименование во время события "onCreated", а скорее:
- Запускает поток, который опрашивает состояние файла до тех пор, пока он не будет заблокирован (используя объект FileInfo).
- Обратный вызов службы для обработки файла, как только она определит, что файл больше не заблокирован и готов к работе
В большинстве случаев сработает простой подход, подобный предложенному @harpo.Используя этот подход, вы можете разработать более сложный код:
- Найдите все открытые дескрипторы для выбранного файла, используя SystemHandleInformation\SystemProcessInformation
- Подкласс WaitHandle class для получения доступа к его внутреннему дескриптору
- Передайте найденные дескрипторы, заключенные в подкласс WaitHandle, в WaitHandle.Метод WaitAny
Объявление для запуска процесса передачи файла с тем же именем, что и у файла Trasferedfile.trg который создается после завершения передачи файла.
Затем настройте FileSystemWatcher, который будет запускать событие только в файле * .trg.
Я не знаю, что вы используете для определения статуса блокировки файла, но что-то вроде этого должно это сделать.
while (true) { try { stream = File.Open( fileName, fileMode ); break; } catch( FileIOException ) { // check whether it's a lock problem Thread.Sleep( 100 ); } }
Возможным решением было бы объединить filesystemwatcher с некоторым опросом,
получайте уведомления о каждом изменении в файле, и при получении уведомления проверяйте, заблокирован ли он как указано в принятом в данный момент ответе: https://stackoverflow.com/a/50800/6754146 Код для открытия filestream скопирован из ответа и слегка изменен:
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);
}
Таким образом, вы можете проверить наличие файла, если он заблокирован, и получать уведомления, когда он закрывается по указанному обратному вызову, таким образом, вы избегаете чрезмерно агрессивного опроса и выполняете работу только тогда, когда она действительно может быть закрыта
Я делаю это так же, как и Гульзар, просто продолжаю пробовать с циклом.
На самом деле я даже не утруждаю себя просмотром файловой системы.Опрашивать сетевой диск на наличие новых файлов раз в минуту - это дешево.
Просто используйте Измененный событие с помощью NotifyFilter Фильтры уведомлений.Последняя запись:
var watcher = new FileSystemWatcher {
Path = @"c:\temp\test",
Filter = "*.xml",
NotifyFilter = NotifyFilters.LastWrite
};
watcher.Changed += watcher_Changed;
watcher.EnableRaisingEvents = true;
Я столкнулся с аналогичной проблемой при добавлении вложения Outlook."Использование" спасло положение.
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);
Как насчет этого как варианта:
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);
}
}
Конечно, если размер файла предварительно выделен при создании, вы получите ложноположительный результат.