ошибка при одновременном доступе к файлу
Столкнулся с проблемой, которую никак не могу решить, прошу помочь знающих.
Задача: по нажатию кнопки показать файл из файлохранилища.
Решение: копируем файл во временную папку, показываем с помощью ShellExecute, удаляем временный файл.
Когда удалять временный файл? Можно, конечно, подождать когда закончится дочерний процесс, запущенный ShellExecute, но как получить его хендл?
К тому же если запускающий процесс закончит работу раньше дочернего, будет нехорошо.
Вообще не удалять, запуская раз в месяц сборщик мусора - тоже не вариант, файлы могут занимать сотни мегабайт.
Пытаюсь решить задачу так: создаю временный, расшаренный на чтение, запись и удаление файл функцией CreateFile с ключом FILE_FLAG_DELETE_ON_CLOSE,
копирую туда данные из файла в хранилище, запускаю ShellExecute, закрываю хэндл файла.
Дочерний процесс пишет, что файл занят другим приложением. Подставив свою прожку определяю код ошибки (GetLastError) после попытки открыть этот файл - 32 (ERROR_SHARING_VIOLATION)
Открываю новый хендл на тот же файл, закрываю старый. Открытие файла дочерним процессом дает ошибку 5 (Access is denied).
Пробовал даже создать дескриптор защиты на полный доступ для всех - результат тот же.
Вот мой код:
{
HANDLE hNew,hOld,hNewRead;
DWORD dwBytesRead, dwBytesWrite, dwFilePos;
char buff[32768];
int z;
hOld=CreateFile(from, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hOld==INVALID_HANDLE_VALUE){MessageBox(0,"Can't open file from file storage","File error", MB_OK+MB_ICONERROR);goto cleanup;}
z=GetLastError();
hNew=CreateFile(to, FILE_APPEND_DATA+GENERIC_READ, FILE_SHARE_READ+FILE_SHARE_WRITE+FILE_SHARE_DELETE, NULL,CREATE_NEW, FILE_ATTRIBUTE_NORMAL+FILE_FLAG_DELETE_ON_CLOSE, hOld);
if (hOld==INVALID_HANDLE_VALUE){MessageBox(0,"Can't create file to temporary folder","File error", MB_OK+MB_ICONERROR);goto cleanup;}
z=GetLastError();
while (ReadFile(hOld,buff,sizeof(buff),&dwBytesRead,NULL)&&dwBytesRead>0)
{
dwFilePos=SetFilePointer(hNew,0,0,FILE_END);
LockFile(hNew,dwFilePos,0,dwBytesRead,0);
WriteFile(hNew,buff,dwBytesRead,&dwBytesWrite,NULL);
UnlockFile(hNew,dwFilePos,0,dwBytesRead,0);
}
CloseHandle(hOld);
hNewRead=CreateFile(to, GENERIC_READ, FILE_SHARE_READ+FILE_SHARE_WRITE+FILE_SHARE_DELETE, NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, hOld);
//для предотвращения удаления файла оставшегося без ссылок
FlushFileBuffers(hNew);
CloseHandle(hNew);
Sleep(500); //если не подождать - файл удалится раньше запуска дочернего процесса
//ShellExecute(0,"open",to,0,0,3);
system("D:\\Документы_Несмачный\\MyProjects\\FileAccessTest\\Debug\\FileAccessTest.exe C:\\temp\\tmp\\zzz.zts");
z=GetLastError();
Sleep(500);
z=GetLastError();
CloseHandle(hNewRead);
}
Помогите заставить работать это чудо правильно.
Кстати,чтобы не было
,можно поступить следующим образом(вариантов масса,на самом деле)–просто не разрешать закрывать менеджер хранилища,пока все документы не будут освобождены.Вдобавок можно при запуске проверять временную папку и чистить её,если что.А файлы открывай через CreateProcess,например
Ещё можно было бы внимательнее прочитать про ShellExecute и увидеть,что[QUOTE=MSDN]To obtain information about the application that is launched as a result of calling ShellExecute, use ShellExecuteEx[/QUOTE]
Можно удалять с помощью *.bat файла, который пытается удалить этот файл и при успешном удалении самоликвидируется. Проблема может возникнуть в тогда, если приложение, которое работает с файлом разрешает его удалять. Есть еще один способ: удалять с помощью MoveFileEx с флагом DELAY_UNTIL_REBOOT, но соответственно получаем проблему - файл нельзя удалить пока не перезагрузишься.
Есть, конечно и более извращенские методы, типа внедрение в чужой поток (explorer, например) и мониторинг родительского приложения. Если оно закрыто, то удаляем файл. Ну и так далее. Удалить можно все, но только через одно место.
Попробовал открывать в дочерней программе точно с теми же правами, как и в запускающей - скопировал строку:
Таже самая ошибка: 5(Access is denied)
Она не всегда возвращает хендл. Как я понял, если программа уже загружена, и она просто заменяет документ на новый, без запуска нового процесса, вместо хендля вернется NULL. И что с таким файлом тогда делать?
Э-э-э... Мне кажется, Вы не совсем правы... Какой смысл в такой реализации этого флага? Суть этого флага в том, что когда все хендлы на файл убиты - файл удаляется. И это работает: стоит закрыть все хэндлы - и файла нет. Моя программа, создающая этот файл, прекрасно обращается к нему на запись (на чтение не проверял, но, думаю, проблем быть не должно), ошибка выплывает именно в обращении из другого процесса. У меня где то ошибка с правами на расшаривание.
Вот именно! Все права только для твоего приложения. Никто не "видит" этот файл кроме тебя, и никогда больше не увидит. Отличное подспорье для временных файлов с конфеденциальной информацией, не так ли?
Алсо, я и не говорил, что удаление не работает. Оно работает и еще как.
АлсоАлсо, попробуй читать файл с привелегиями System, если код ошибки 5 (Access is denied). Но и то, сомневаюсь я, что взлетит. Когда я ковырялся со своей проблемой, привелегии system были недопустимы для меня и пробовать не стал тогда.
void WINAPI ViewFile(LPCTSTR from, LPCTSTR szTempFileName) {
DWORD TId = 0;
HANDLE hThread = INVALID_HANDLE_VALUE;
CopyFile(from, szTempFileName, FALSE); // создали временные файл
hThread = CreateThread(NULL, 0, ViewFileThread, (LPVOID)szTempFileName, 0, &TId);
WaitForSingleObject(hThread, INFINITE);
DeleteFile(szTempFileName);
}
DWORD WINAPI ViewFileThread(LPVOID lpParam) {
LPCTSTR szTempFileName = (LPCTSTR)lpParam;
TCHAR szCmdLine[MAX_PATH] = {0};
StringCbCopy(szCmdLine, MAX_PATH, _T("cmd.exe /c start /wait "));
StringCbCat(szCmdLine, MAX_PATH, szTempFileName);
PROCESS_INFORMATION processInformation;
STARTUPINFO startupInfo;
ZeroMemory(&processInformation, sizeof(PROCESS_INFORMATION));
ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
startupInfo.cb = sizeof(startupInfo);
CreateProcess(NULL, szCmdLine, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL, &startupInfo, &processInformation);
WaitForSingleObject(processInformation.hProcess, INFINITE);
CloseHandle(processInformation.hProcess);
CloseHandle(processInformation.hThread);
return 0;
}
//бла бла бла
...
Извините, конечно, может я не понимаю замысла ТС, но наверно CreateThread не подойдет. В первом же сообщение упоминались файлы в несколько мегабайт. Сомневаюсь, что генерируются экзешники такого размера. Да и ShellExecute как бы намекает, что файл будет открываться ассоциированной с ним програмой.
Алсо, если не нравятся наши костыли, то как вы будете изварачиваться из проблемы, когда основное приложение закрыто? А такая проблема поднималась в первом же сообщением. Как быть тогда?
А какая связь между CreateThread() и размером открываемого файла?
Да и ShellExecute как бы намекает, что файл будет открываться ассоциированной с ним програмой.
Согласен, но ShellExecute() возвращается сразу после запуска, не позволяя дождаться завершения, в отличие от start /wait ....
Алсо, если не нравятся наши костыли, то как вы будете изварачиваться из проблемы, когда основное приложение закрыто? А такая проблема поднималась в первом же сообщением. Как быть тогда?
В первом сообщении также говорилось, что
[QUOTE=imerlin]
если запускающий процесс закончит работу раньше дочернего, будет нехорошо.
[/QUOTE]
А следовательно, перед завершением основного процесса я закрою все дополнительные нити и процессы - скорее всего, с помощью соответствующих объектов синхронизации.
Понял, как вы стали решать этот вопрос.
Проверил ваш метод. И получил еще одни грабли.
Открывал *.txt файл. Открыл. Работал отлично.
Эксперимент 2:
Открывал *.html файл при закрытом браузере (пробывал в двух IE и FF). Открылся и работал метод отлично.
Эксперимент 3:
Открыл *.html файл при открытом браузере (снова оба). И тут крах: открылась отдельная вкладка, консоль закрылась.
Эксперимент 4:
Открыл *.doc файл с открытым редактором. И тут снова крах.
Считаю ваш метод тоже тем еще костылем. Практика подтверждает.
Прежде всего, у меня вопрос к ТСу: файлы каких типов будут открываться? Если каких-то фиксированных, то всегда можно дописать свой менеджер, который подменит стандартный start /wait и решит все проблемы. Если никакого ограничения на тип открываемого файла нет, боюсь, что проблема в поставленном виде не разрешима в принципе.
2 hivewarrior
Грабли лежат в том, что не факт, что когда файл открыт в какой-нибудь программе (к примеру, в блокноте), существует его открытый дескриптор, на который можно ориентироваться. Тот же блокнот, например, сразу закрывает дескриптор, специально проверил утилитами из SysInternals. Таким образом, после запуска имеем (в общем случае) некий сторонний процесс, в котором открыт наш файл, все дескрипторы к которому могут быть как закрыты, так и открыты. На что здесь можно повеситься? Разве что попытаться в лоб сделать:
CopyFile(from, szTempFileName, FALSE); // создали временный файл
ShellExecute(NULL, "open", szTempFileName, NULL, NULL, SW_SHOW);
Sleep(1000);
DeleteFile(szTempFileName);
}
В некоторых случаях поможет мой первый вариант, в некоторых - этот. Если их скомбинировать, вероятность правильной работы будет достаточно высока.
P.S. В качестве доказательства, что поставленная проблема отнюдь не тривиальна, могу привести следующий пример. Попробуйте открыть, например, в Total Commander'е какой-нибудь *.html-файл из архива. Увидите окно следующего содержания:
[ATTACH=CONFIG]5170[/ATTACH]
Разные приложения работают по-разному. Некоторые вообще могут мапить файлы в память. И тут снова придется плясать с бубном.
Второй способ вообще странный какой-то. То есть открывается какой-то отчет (я так понимаю, раз большие файлы, то это отчеты к чему-либо), то его смотреть иногда хочется подольше, чем секунду. И этот влоб ничем не отличается от вашего первого предложения по большому счету.
Вопрос очень интересный на самом деле. Там подводных камней куча. Скорее всего придется решать его частными способами, а потом их как-то обрабатывать. Да и не помешало бы вообще иметь сервис, который бы мониторил временную дирректорию...
Второй способ вообще странный какой-то. То есть открывается какой-то отчет (я так понимаю, раз большие файлы, то это отчеты к чему-либо), то его смотреть иногда хочется подольше, чем секунду.
Речь идет не о том немного. Я имею в виду, что некоторое приложение загружает файл, показывает пользователю и не нуждается в его дальнейшем физическом присутствии на диске. Пример - опять же, блокнот. Хотя этот способ мне самому абсолютно не нравится, потому что не использует никаких средств синхронизации, это лишь маленький набросок.
этот влоб ничем не отличается от вашего первого предложения по большому счету.
Да, готов согласиться. Здесь погорячился.
Вопрос очень интересный на самом деле. Там подводных камней куча. Скорее всего придется решать его частными способами, а потом их как-то обрабатывать. Да и не помешало бы вообще иметь сервис, который бы мониторил временную дирректорию...
Да, вопрос действительно интересный, не спорю.
А что даст такой сервис? Что он должен делать, по-вашему? Просто смотрите, я еще раз повторюсь: в общем случае, зная только имя файла, мы не имеем никакой информации, использует ли кто-то этот файл в данный момент. И второй вопрос: вы можете привести принципиальный способ (о конкретной реализации потом поговорим), как можно, например, отследить закрытие нужной вкладки в IE или ФФ? И как идентифицировать нужную вкладку при открытии?
А что даст такой сервис? Что он должен делать, по-вашему? Просто смотрите, я еще раз повторюсь: в общем случае, зная только имя файла, мы не имеем никакой информации, использует ли кто-то этот файл в данный момент.
Ну я бы не стал так уверенно утверждать, что зная имя, мы ничего не имеем. Есть еще куча информации, которая собирается системой и на часть мы можем глянуть. Можно мониторить время последнего обращения к файлу и по истечению определенного времени удалять его, например. Делать это все для каждого файла в отдельном треде и так далее... Мониторить только временную дирректорию и так далее. Тут уже фантазией только ограничивается. Сервис работает всегда (если руками ничего не трогать). А страховка от перезагрузки: MoveFileEx в null с флагом DELAY_UNTIL_REBOOT
Ну в первом сообщении ТС писал, что удалять по времени - не вариант. Я все-таки упорно жду от него ответа, какие именно файлы предстоит открывать. Возможно, весь бубен, который мы сейчас обсуждаем, и не понадобится.
Ну он не хотел "запускать раз в месяц" что-то. Очевидно из просьбы, это надо делать самому и вообще. А сервис шустро мониторит файлы и при отсутствии обращений к нему за час удаляет его. Час и месяц - вполне разнича. Но ждать ответа ТС действительно стоит. Может, и в правду зря бисер здесь мечем.
Вопрос в том, что возможна ситуация, когда пользователь открыл какой-то файл и ушел на два часа. :) С точки зрения системы новых обращений нет, а на экране он висит. Поэтому я и говорю, что здесь нужно либо в принципе отходить от "потаймерной" обработки, либо подбирать для каждого конкретного пользователя разумный интервал очистки, что не вполне красиво, согласитесь.
Если на сервере это все крутится, то можно в 0-00 очищать папочку, например. Если не на сервере и компьютер выключают, то, как я уже говорил, привентивный MoveFileEx в null с флагом DELAY_UNTIL_REBOOT
Согласен. Это вообще камень предкновения всех и вся более или менее серьезных разработок. Как же поменьше нагадить в системе. Чистить за собой реестр, файлы и прочее, прочее.
Всем огромное спасибо за проявленный интерес.
Файлы, увы, любых типов, хотя в 99% случаев ожидаются большие графические файлы - тифы и джипеги.
Не пойдет: надо чтобы система сама открыла файл ассоциированной с таким типом файлов программой.
В принципе ShellExecuteEx отдает хендл процесса, но не во всех случаях, только когда порождает новый процесс, а если просто открывает файл в новом окне/вкладке/заменяя предыдущий файл - увы, возвращает NULL.
Кроме того так все равно не получить сообщение об освобождении ранее запущенного файла.
Не совсем... База данных хранит (наряду с другой информацией) асооциированные с этой информацией файлы. Для надежности они лежат в недоступном пользователям хранилище с техническими именами и, в будущем, возможно будут
кодироваться (мания такая у начальства ;-)). Но по запросу авторизованного пользователя файл ему надо показать. Для этого файл должен быть скопирован во временную папку с восстановленным именем и применена функция
ShellExecute. Потом файл надо удалить. Файлы могут быть самыми разными, но в основном большие растровые или векторные карты. Если пользователь знает, что к некоторому объекту прикреплена какая то карта, он может захотеть
быстро перебрать все прикрепленные графические файлы в поисках нужного, а на анализ изображения у человека уходят доли секунды, поэтому перебирать может быстро, до тех пор, пока не наткнется на нужный файл.
По поводу моего первоначального вопроса я уже разобрался: нельзя открывать файл, созданный с ключом DELETE_ON_CLOSE без расшаренных прав на удаление, что от стороннего приложения требовать нельзя, поэтому такой путь отпадает.
Пока решил вопрос так: написал два экзешника. Один крутится и раз в 5 сек удаляет все файлы в папке, назначенной для этого безобразия (по GetTempPath, плюс создать подпапку для хулиганства). Если файл не удалился - значит занят и
пусть живет пока. Второй экзешник управляет первым с помощью именованных событий, он умеет стартовать/выгружать/ставить на паузу/снимать с паузы первый. Когда второй экзешник создает событие для остановки первого, первый
закончит работу после удаления последнего файла в папке.
Не айс, конечно, лишний процесс крутится, процессорное время жрет, а главное - диск постоянно тормозит, но пока ничего лучше не придумал...
Как я уже писал, отслеживание выполняющегося потока - не вариант: нет гарантии, что поток завершится вместе с просмотром файла и запустится с просмотром нового.
Еще раз всем огромное спасибо, может лучше идеи появятся? ;-)