Есть ли способ проверить, используется ли файл?
Я пишу программу на C#, которая должна повторно получить доступ к 1 файлу изображения. В большинстве случаев это работает, но если мой компьютер работает быстро, он попытается получить доступ к файлу до его сохранения в файловой системе и выдаст ошибку: "Файл используется другим процессом".
Я хотел бы найти способ обойти это, но все мои поиски в Google привели только к созданию проверок с использованием обработки исключений. Это против моей религии, поэтому мне было интересно, есть ли у кого-нибудь лучший способ сделать это?
20 ответов
Обновлено ПРИМЕЧАНИЕ об этом решении: проверка с помощью FileAccess.ReadWrite
произойдет сбой для файлов только для чтения, поэтому решение было изменено для проверки с помощью FileAccess.Read
, Хотя это решение работает, потому что пытается проверить с FileAccess.Read
потерпит неудачу, если файл имеет блокировку записи или чтения, однако, это решение не будет работать, если файл не имеет блокировки записи или чтения, т.е. он был открыт (для чтения или записи) с помощью FileShare. Чтение или доступ FileShare.Write.
ОРИГИНАЛ: Я использовал этот код в течение последних нескольких лет, и у меня не было никаких проблем с ним.
Поймите, что вы колеблетесь с использованием исключений, но вы не можете избежать их все время:
protected virtual bool IsFileLocked(FileInfo file)
{
FileStream stream = null;
try
{
stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None);
}
catch (IOException)
{
//the file is unavailable because it is:
//still being written to
//or being processed by another thread
//or does not exist (has already been processed)
return true;
}
finally
{
if (stream != null)
stream.Close();
}
//file is not locked
return false;
}
Вы можете страдать от состояния гонки потоков, в котором есть документированные примеры использования в качестве уязвимости безопасности. Если вы проверите, что файл доступен, но затем попытаетесь использовать его, вы можете выбросить его в этот момент, который злоумышленник может использовать для принудительного использования и использования вашего кода.
Ваша лучшая ставка - это попытка catch / finally, которая пытается получить дескриптор файла.
try
{
using (Stream stream = new FileStream("MyFilename.txt", FileMode.Open))
{
// File/Stream manipulating code here
}
} catch {
//check here why it failed and ask user to retry if the file is in use.
}
Используйте это, чтобы проверить, заблокирован ли файл:
using System.IO;
using System.Runtime.InteropServices;
internal static class Helper
{
const int ERROR_SHARING_VIOLATION = 32;
const int ERROR_LOCK_VIOLATION = 33;
private static bool IsFileLocked(Exception exception)
{
int errorCode = Marshal.GetHRForException(exception) & ((1 << 16) - 1);
return errorCode == ERROR_SHARING_VIOLATION || errorCode == ERROR_LOCK_VIOLATION;
}
internal static bool CanReadFile(string filePath)
{
//Try-Catch so we dont crash the program and can check the exception
try {
//The "using" is important because FileStream implements IDisposable and
//"using" will avoid a heap exhaustion situation when too many handles
//are left undisposed.
using (FileStream fileStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) {
if (fileStream != null) fileStream.Close(); //This line is me being overly cautious, fileStream will never be null unless an exception occurs... and I know the "using" does it but its helpful to be explicit - especially when we encounter errors - at least for me anyway!
}
}
catch (IOException ex) {
//THE FUNKY MAGIC - TO SEE IF THIS FILE REALLY IS LOCKED!!!
if (IsFileLocked(ex)) {
// do something, eg File.Copy or present the user with a MsgBox - I do not recommend Killing the process that is locking the file
return false;
}
}
finally
{ }
return true;
}
}
Из соображений производительности я рекомендую прочитать содержимое файла в той же операции. Вот некоторые примеры:
public static byte[] ReadFileBytes(string filePath)
{
byte[] buffer = null;
try
{
using (FileStream fileStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
int length = (int)fileStream.Length; // get file length
buffer = new byte[length]; // create buffer
int count; // actual number of bytes read
int sum = 0; // total number of bytes read
// read until Read method returns 0 (end of the stream has been reached)
while ((count = fileStream.Read(buffer, sum, length - sum)) > 0)
sum += count; // sum is a buffer offset for next reading
fileStream.Close(); //This is not needed, just me being paranoid and explicitly releasing resources ASAP
}
}
catch (IOException ex)
{
//THE FUNKY MAGIC - TO SEE IF THIS FILE REALLY IS LOCKED!!!
if (IsFileLocked(ex))
{
// do something?
}
}
catch (Exception ex)
{
}
finally
{
}
return buffer;
}
public static string ReadFileTextWithEncoding(string filePath)
{
string fileContents = string.Empty;
byte[] buffer;
try
{
using (FileStream fileStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
int length = (int)fileStream.Length; // get file length
buffer = new byte[length]; // create buffer
int count; // actual number of bytes read
int sum = 0; // total number of bytes read
// read until Read method returns 0 (end of the stream has been reached)
while ((count = fileStream.Read(buffer, sum, length - sum)) > 0)
{
sum += count; // sum is a buffer offset for next reading
}
fileStream.Close(); //Again - this is not needed, just me being paranoid and explicitly releasing resources ASAP
//Depending on the encoding you wish to use - I'll leave that up to you
fileContents = System.Text.Encoding.Default.GetString(buffer);
}
}
catch (IOException ex)
{
//THE FUNKY MAGIC - TO SEE IF THIS FILE REALLY IS LOCKED!!!
if (IsFileLocked(ex))
{
// do something?
}
}
catch (Exception ex)
{
}
finally
{ }
return fileContents;
}
public static string ReadFileTextNoEncoding(string filePath)
{
string fileContents = string.Empty;
byte[] buffer;
try
{
using (FileStream fileStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
int length = (int)fileStream.Length; // get file length
buffer = new byte[length]; // create buffer
int count; // actual number of bytes read
int sum = 0; // total number of bytes read
// read until Read method returns 0 (end of the stream has been reached)
while ((count = fileStream.Read(buffer, sum, length - sum)) > 0)
{
sum += count; // sum is a buffer offset for next reading
}
fileStream.Close(); //Again - this is not needed, just me being paranoid and explicitly releasing resources ASAP
char[] chars = new char[buffer.Length / sizeof(char) + 1];
System.Buffer.BlockCopy(buffer, 0, chars, 0, buffer.Length);
fileContents = new string(chars);
}
}
catch (IOException ex)
{
//THE FUNKY MAGIC - TO SEE IF THIS FILE REALLY IS LOCKED!!!
if (IsFileLocked(ex))
{
// do something?
}
}
catch (Exception ex)
{
}
finally
{
}
return fileContents;
}
Попробуйте сами:
byte[] output1 = Helper.ReadFileBytes(@"c:\temp\test.txt");
string output2 = Helper.ReadFileTextWithEncoding(@"c:\temp\test.txt");
string output3 = Helper.ReadFileTextNoEncoding(@"c:\temp\test.txt");
Недавно я столкнулся с этой проблемой и обнаружил следующее: https://docs.microsoft.com/en-us/dotnet/standard/io/handling-io-errors.
Здесь Microsoft описывает следующий метод проверки наличия
IOException
произошло из-за заблокированного файла:
catch (IOException e) when ((e.HResult & 0x0000FFFF) == 32 ) {
Console.WriteLine("There is a sharing violation.");
}
Возможно, вы могли бы использовать FileSystemWatcher и наблюдать за событием Changed.
Я сам этим не пользовался, но, возможно, стоит попробовать. Если файловый системный наблюдатель окажется немного тяжелым для этого случая, я бы пошел на цикл try/catch/sleep.
Просто используйте исключение по назначению. Подтвердите, что файл используется, и повторите попытку несколько раз, пока ваше действие не будет завершено. Это также наиболее эффективно, потому что вы не тратите время на проверку состояния, прежде чем действовать.
Используйте функцию ниже, например
TimeoutFileAction(() => { System.IO.File.etc...; return null; } );
Метод многократного использования через 2 секунды
private T TimeoutFileAction<T>(Func<T> func)
{
var started = DateTime.UtcNow;
while ((DateTime.UtcNow - started).TotalMilliseconds < 2000)
{
try
{
return func();
}
catch (System.IO.IOException exception)
{
//ignore, or log somewhere if you want to
}
}
return default(T);
}
Принятые ответы, приведенные выше, страдают проблемой, когда файл был открыт для записи в режиме FileShare.Read или если файл имеет атрибут "Только для чтения", код не будет работать. Это модифицированное решение работает наиболее надежно, имея в виду две вещи (как и для принятого решения):
- Он не будет работать для файлов, которые были открыты в режиме общего ресурса записи
- Это не учитывает проблемы с многопоточностью, поэтому вам придется заблокировать их или обрабатывать проблемы с потоками отдельно.
Помня вышеупомянутое, это проверяет, заблокирован ли файл для записи или заблокирован, чтобы предотвратить чтение:
public static bool FileLocked(string FileName)
{
FileStream fs = null;
try
{
// NOTE: This doesn't handle situations where file is opened for writing by another process but put into write shared mode, it will not throw an exception and won't show it as write locked
fs = File.Open(FileName, FileMode.Open, FileAccess.ReadWrite, FileShare.None); // If we can't open file for reading and writing then it's locked by another process for writing
}
catch (UnauthorizedAccessException) // https://msdn.microsoft.com/en-us/library/y973b725(v=vs.110).aspx
{
// This is because the file is Read-Only and we tried to open in ReadWrite mode, now try to open in Read only mode
try
{
fs = File.Open(FileName, FileMode.Open, FileAccess.Read, FileShare.None);
}
catch (Exception)
{
return true; // This file has been locked, we can't even open it to read
}
}
catch (Exception)
{
return true; // This file has been locked
}
finally
{
if (fs != null)
fs.Close();
}
return false;
}
Вы можете вернуть задачу, которая дает вам поток, как только он становится доступным. Это упрощенное решение, но это хорошая отправная точка. Это потокобезопасно.
private async Task<Stream> GetStreamAsync()
{
try
{
return new FileStream("sample.mp3", FileMode.Open, FileAccess.Write);
}
catch (IOException)
{
await Task.Delay(TimeSpan.FromSeconds(1));
return await GetStreamAsync();
}
}
Вы можете использовать этот поток как обычно:
using (var stream = await FileStreamGetter.GetStreamAsync())
{
Console.WriteLine(stream.Length);
}
Вот код, который, насколько я могу судить, делает то же самое, что и принятый ответ, но с меньшим количеством кода:
public static bool IsFileLocked(string file)
{
try
{
using (var stream = File.OpenRead(file))
return false;
}
catch (IOException)
{
return true;
}
}
Однако я думаю, что это более надежно сделать следующим образом:
public static void TryToDoWithFileStream(string file, Action<FileStream> action,
int count, int msecTimeOut)
{
FileStream stream = null;
for (var i = 0; i < count; ++i)
{
try
{
stream = File.OpenRead(file);
break;
}
catch (IOException)
{
Thread.Sleep(msecTimeOut);
}
}
action(stream);
}
static bool FileInUse(string path)
{
try
{
using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate))
{
fs.CanWrite
}
return false;
}
catch (IOException ex)
{
return true;
}
}
string filePath = "C:\\Documents And Settings\\yourfilename";
bool isFileInUse;
isFileInUse = FileInUse(filePath);
// Then you can do some checking
if (isFileInUse)
Console.WriteLine("File is in use");
else
Console.WriteLine("File is not in use");
Надеюсь это поможет!
Помимо работы с 3-строчками и просто для справки: если вы хотите получить полную информацию - в Microsoft Dev Center есть небольшой проект:
https://code.msdn.microsoft.com/windowsapps/How-to-know-the-process-704839f4
Из введения:
Пример кода на C#, разработанный в.NET Framework 4.0, поможет выяснить, какой процесс блокирует файл. Функция RmStartSession, включенная в rstrtmgr.dll, использовалась для создания сеанса менеджера перезапуска, и в соответствии с возвращаемым результатом создается новый экземпляр объекта Win32Exception. После регистрации ресурсов в сеансе Restart Manager с помощью функции RmRegisterRescources вызывается функция RmGetList для проверки того, какие приложения используют определенный файл, путем перечисления массива RM_PROCESS_INFO.
Он работает, подключившись к "Перезапустить сеанс менеджера".
Диспетчер перезапуска использует список ресурсов, зарегистрированных в сеансе, чтобы определить, какие приложения и службы должны быть закрыты и перезапущены. Ресурсы можно идентифицировать по именам файлов, коротким именам служб или структурам RM_UNIQUE_PROCESS, которые описывают работающие приложения.
Это может быть немного слишком сложно для ваших конкретных потребностей... Но если это то, что вы хотите, продолжайте и возьмите vs-проект.
Единственный известный мне способ - использовать API эксклюзивной блокировки Win32, который не слишком быстр, но примеры существуют.
Большинство людей, для простого решения этой проблемы, просто пробуют / ловят / спят петлями.
Когда-то мне нужно было загружать PDF-файлы в онлайн-архив. Но резервное копирование не удастся, если пользователь откроет файл в другой программе (например, в программе чтения PDF). В спешке я попытался ответить на несколько лучших вопросов в этой теме, но не смог заставить их работать. Что мне помогло, так это попытка переместить файл PDF в его собственный каталог. Я обнаружил, что это не получится, если файл будет открыт в другой программе, и если перемещение будет успешным, операция восстановления не потребуется, как если бы он был перемещен в отдельный каталог. Я хочу опубликовать свое базовое решение на случай, если оно может быть полезно для других конкретных случаев использования.
string str_path_and_name = str_path + '\\' + str_filename;
FileInfo fInfo = new FileInfo(str_path_and_name);
bool open_elsewhere = false;
try
{
fInfo.MoveTo(str_path_and_name);
}
catch (Exception ex)
{
open_elsewhere = true;
}
if (open_elsewhere)
{
//handle case
}
По моему опыту, вы обычно хотите сделать это, затем "защитить" свои файлы, чтобы сделать что-то причудливое, а затем использовать "защищенные" файлы. Если у вас есть только один файл, который вы хотите использовать подобным образом, вы можете воспользоваться трюком, объясненным в ответе Джереми Томпсона. Однако, если вы попытаетесь сделать это для большого количества файлов (скажем, например, когда вы пишете установщик), вам будет очень больно.
Очень элегантный способ решить эту проблему - использовать тот факт, что ваша файловая система не позволит вам изменить имя папки, если один из файлов там используется. Держите папку в той же файловой системе, и она будет работать как шарм.
Обратите внимание, что вы должны знать об очевидных способах его использования. В конце концов, файлы не будут заблокированы. Также имейте в виду, что есть и другие причины, которые могут привести к Move
операция не удалась. Очевидно, что правильная обработка ошибок (MSDN) может помочь здесь.
var originalFolder = @"c:\myHugeCollectionOfFiles"; // your folder name here
var someFolder = Path.Combine(originalFolder, "..", Guid.NewGuid().ToString("N"));
try
{
Directory.Move(originalFolder, someFolder);
// Use files
}
catch // TODO: proper exception handling
{
// Inform user, take action
}
finally
{
Directory.Move(someFolder, originalFolder);
}
Что касается отдельных файлов, я бы придерживался предложения о блокировке от Джереми Томпсона.
Вы можете использовать мою библиотеку для доступа к файлам из нескольких приложений.
Вы можете установить его из nuget: Install-Package Xabe.FileLock
Если вы хотите больше информации об этом, проверьте https://github.com/tomaszzmuda/Xabe.FileLock
ILock fileLock = new FileLock(file);
if(fileLock.Acquire(TimeSpan.FromSeconds(15), true))
{
using(fileLock)
{
// file operations here
}
}
Метод fileLock.Acquire вернет true, только если может заблокировать файл исключительно для этого объекта. Но приложение, которое загружает файл, должно делать это и при блокировке файла. Если объект недоступен, метод возвращает false.
Поможет ли что-нибудь подобное?
var fileWasWrittenSuccessfully = false;
while (fileWasWrittenSuccessfully == false)
{
try
{
lock (new Object())
{
using (StreamWriter streamWriter = new StreamWriter(filepath.txt"), true))
{
streamWriter.WriteLine("text");
}
}
fileWasWrittenSuccessfully = true;
}
catch (Exception)
{
}
}
Попробуйте переместить / скопировать файл во временную папку. Если вы можете, у него нет блокировки, и вы можете безопасно работать в временном каталоге, не получая блокировки. В противном случае просто попробуйте переместить его снова через x секунд.
Мне интересно посмотреть, вызывает ли это какие-либо рефлексы WTF. У меня есть процесс, который создает и впоследствии запускает документ PDF из консольного приложения. Однако я имел дело с уязвимостью, когда, если бы пользователь запускал процесс несколько раз, генерируя один и тот же файл, не закрывая предварительно сгенерированный файл, приложение выдает исключение и умирает. Это было довольно частым явлением, потому что имена файлов основаны на номерах коммерческих предложений.
Вместо того, чтобы терпеть неудачу в такой неблагодарной манере, я решил положиться на автоматическое добавление версий файлов:
private static string WriteFileToDisk(byte[] data, string fileName, int version = 0)
{
try
{
var versionExtension = version > 0 ? $"_{version:000}" : string.Empty;
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"{fileName}{versionExtension}.pdf");
using (var writer = new FileStream(filePath, FileMode.Create))
{
writer.Write(data, 0, data.Length);
}
return filePath;
}
catch (IOException)
{
return WriteFileToDisk(data, fileName, ++version);
}
}
Возможно, еще немного внимания можно уделить catch
блок, чтобы убедиться, что я ловлю правильные исключения IOException. Я, вероятно, также очистлю хранилище приложения при запуске, так как эти файлы в любом случае должны быть временными.
Я понимаю, что это выходит за рамки вопроса ОП просто проверять, используется ли файл, но это действительно была проблема, которую я искал, когда прибыл сюда, так что, возможно, это будет полезно для кого-то еще.
Я использую этот обходной путь, но у меня есть промежуток времени между тем, когда я проверяю блокировку файла с помощью функции IsFileLocked, и когда я открываю файл. В этот промежуток времени файл может открыть какой-то другой поток, поэтому я получу IOException.
Итак, я добавил дополнительный код для этого. В моем случае я хочу загрузить XDocument:
XDocument xDoc = null;
while (xDoc == null)
{
while (IsFileBeingUsed(_interactionXMLPath))
{
Logger.WriteMessage(Logger.LogPrioritet.Warning, "Deserialize can not open XML file. is being used by another process. wait...");
Thread.Sleep(100);
}
try
{
xDoc = XDocument.Load(_interactionXMLPath);
}
catch
{
Logger.WriteMessage(Logger.LogPrioritet.Error, "Load working!!!!!");
}
}
Как вы думаете? Могу ли я что-то изменить? Может быть, мне вообще не приходилось использовать функцию IsFileBeingUsed?
Спасибо
retry_possibility:
//somecode here
try
{
using(FileStream stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None))
{
stream.Close();
}
//write or open your file here
}
catch (IOException)
{
DialogResult dialogResult = MessageBox.Show("This file is opened by you or another user. Please close it and press retry.\n"+ expFilePath, "File Locked", MessageBoxButtons.RetryCancel);
if (dialogResult == DialogResult.Retry)
{
goto retry_possibility;
}
else if (dialogResult == DialogResult.Cancel)
{
//do nothing
}
}