题
在文件被解锁并且可以读取和重命名之前阻塞线程的最简单方法是什么?例如,.NET Framework 中是否有 WaitOnFile() ?
我有一个使用 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;
}
其他提示
从 Eric 的回答开始,我进行了一些改进,使代码更加紧凑和可重用。希望它有用。
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 公开,因此我添加了一个小 hack 函数 (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
}
它使用命名互斥体来工作。那些希望访问该文件的人会尝试获取对命名互斥体的控制权,该互斥体共享文件名(“\”变成“/”)。您可以使用 Open(),它会停止直到互斥体可访问,也可以使用 TryOpen(TimeSpan),它尝试在给定的持续时间内获取互斥体,如果无法在时间跨度内获取则返回 false。这很可能应该在 using 块内使用,以确保正确释放锁,并且在释放该对象时正确释放流(如果打开)。
我用大约 20 个东西做了一个快速测试,对文件进行各种读/写操作,没有发现任何损坏。显然它不是很先进,但它应该适用于大多数简单的情况。
对于这个特定的应用程序,直接观察文件将不可避免地导致难以追踪的错误,特别是当文件大小增加时。这里有两种不同的有效策略。
- ftp 两个文件但只能观看其中一个。例如发送文件 important.txt 和 important.finish。只关注完成文件,但处理 txt。
- FTP 一个文件,但完成后重命名。例如,发送 important.wait 并让发件人在完成后将其重命名为 important.txt。
祝你好运!
我不久前使用的技术之一是编写自己的函数。基本上捕获异常并使用计时器重试,您可以在指定的时间内触发该计时器。如果有更好的方法,欢迎分享。
从 微软软件定义网络:
创建文件后立即升起发射的事件。如果将文件复制或传输到观察目录中,则将立即提出爆发的事件,然后进行一次或多个on onchange事件。
您的 FileSystemWatcher 可以进行修改,以便它不会在“OnCreated”事件期间执行读取/重命名,而是:
- Spanws 一个线程,轮询文件状态直到它未被锁定(使用 FileInfo 对象)
- 一旦确定文件不再锁定并准备就绪,就会回调服务来处理文件
在大多数情况下,像 @harpo 建议的简单方法会起作用。您可以使用此方法开发更复杂的代码:
- 使用 SystemHandleInformation\SystemProcessInformation 查找所选文件的所有打开的句柄
- 子类 WaitHandle 类以访问其内部句柄
- 将找到的包含在子类 WaitHandle 中的句柄传递给 WaitHandle.WaitAny 方法
AD转移过程触发文件samenameastrasferedfile.trg是在文件传输完成后创建的。
然后设置 FileSystemWatcher,仅在 *.trg 文件上触发事件。
我不知道你用什么来确定文件的锁定状态,但类似这样的东西应该可以做到。
while (true) { try { stream = File.Open( fileName, fileMode ); break; } catch( FileIOException ) { // check whether it's a lock problem Thread.Sleep( 100 ); } }
一个可能的解决方案是将文件系统观察器与一些轮询结合起来,
通知文件上的每一个更改,并在通知时检查是否按照当前接受的答案中的规定锁定: https://stackoverflow.com/a/50800/6754146打开文件流的代码是从答案中复制并稍加修改的:
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);
}
通过这种方式,您可以检查文件是否已锁定,并在通过指定的回调关闭时收到通知,这样您就可以避免过度激进的轮询,并且仅在文件可能实际关闭时才执行工作
我的做法和 Gulzar 一样,只是不断尝试循环。
事实上,我什至不关心文件系统观察程序。每分钟轮询一次网络驱动器以查找新文件的成本很低。
只需使用 改变了 带有 NotifyFilter 的事件 通知过滤器.LastWrite:
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);
}
}
当然,如果文件大小是在创建时预先分配的,您会得到误报。