Детективная история со счастливым концом
Mar. 6th, 2013 11:54 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Пердуперждение: эта запись вряд ли будет понятна непрограммистам. А на программистов может навеять скуку... или не навеять.
Иногда при кодировании случаются ситуёвины, напоминающие детектив. Вчера такое вот случилось.
Ковыряюсь я в своем звуковом редакторе "Bard". Как полигон для тренировки и освоения разных технологий - очень даже зачетная вещь. Вчера захотелось мне странного: чтобы два преобразования музыкальных файлов шли в параллель.
Преобразования у меня трех типов - built-in, на основе ACM и на основе любимой многими утилиты FFmpeg. Вот о последней и пойдет речь.
Я эту FFmpeg имел в самых разных позах - например, работал прилагаемой к ней с ней библиотекой кодеков через ейное API, собирал саму утилиту как модуль-часть-программы и как отдельную подгружаемую на runtime DLL-ку с кастрированием лишних функций... как-то все не то. И самый последний вид сношений, к которому я прибег, это использование pipe-ов.
Вкратце суть проблемы была такой: на вход одного преобразования подается файл Alice.wav, на выходе ождижается Alice1.mp3. Второе преобразование на вход берет тот же Alice.wav, на выходе - Alice2.mp3.
И вот... запускаю я два преобразования одновременно (с интервалом в пару секунд) и совершенно внезапно программа выдает ошибку: "доступ запрещен, файл: Alice1.mp3". Ну, я, естественно, лезу в стек, и обнаруживаю, что сбойнула функция rename. Кто бы мог подумать, какой длиннющий детектив породит такая ерунда.
Небольшое лирическое отступление. Правильные пацаны не подают на преобразователь выходной файл, т.к. преобразование может идти поверх прежней версии файла (с перезаписью), сбойнуть, и в результате получится полуперезаписанный файл - и не тот, что прежде, и не тот, что ожидается. Поэтому вместо прямой записи в Alice1.mp3 идет запись во временный файл, а потом этот временный подменяется через переименования. То есть, как-то вот так:
Благодаря этой схеме, при ошибках и даже краше на любом этапе в живых останется либо прежняя версия файла, либо новая, либо обе. До последнего del возможен откат.
Ну так вот... на этой схеме виден тот самый сбойный "rename Alice1.mp3 -> temp2.tmp".
И начался детектив...
Первое предположение было - "сам дурак". Подозрение на модуль защиты. Я, видите ли, немножко параноик. И я баюс-баюс, что пока ffmpeg вдумчиво и печально ковыряет мегабайты, какая-нибудь сцукка-пользователь возьмет и что-нибудь сделает с Alice1.mp3. Например, возьмет и захочет проиграть старую версию в winamp-е или еще где, и когда преобразование завершится, файл окажется заблокирован на запись. Так что я в начале преобразования открываю этот файл с запретом записи. А потом, перед тем самым rename закрываю файл. Так вот заподозрил я, что файл не закрывается, не освобождается. Проверял и так, и эдак - все вроде бы нормально. Должен освободиться. Останавливал программу перед самым rename и переименовывал из консоли - все окей. То есть, файл оставался заблокированным всего пару секунд или меньше. Трехсекундный Sleep перед rename - и все начинает работать.
Вторым подозреваемым стал антивирус. Я подумал: а вдруг этой сцукке внезапно приспичило просканировать файл? Бывало уже такое. Отключение антивируса не помогло. Что за фигня?
И тут я вспомнил, что есть у меня качественнаятрава тулза от товарища Марка Руссиновича, назывется handle. Эта лапочка может сказать мне, какая гадюка держит файл.
Проблема только в том, что удержание файла длилось недолго, после чего переименование происходило спокойно. Как поймать момент?
Решение пришло в виде строчки кода:
Маленькая хитрость: "cmd /K " запускает утилиту handle в отдельной консоли и НЕ закрывает консоль по окончанию работы, так, чтобы я успел все спокойно посмотреть.
И вскрылась страшная тайна! (тревожная музыка)
То есть, файл держит утилита ffmpeg. Но как, Холмс?! Если посмотреть выше на схемку преобразований, ffmpeg вообще ничего не знает про Alice1.mp3, она пишет в другой, временный файл (что я немедленно проверил с помощью OutputDebugString).
Я еще попробовал запустить handle с опцией -p, посмотреть, какие файлы вообще держатся моими процессами в этот момент. И получилось, что:
Bard держит открытым Alice.wav (для защиты), Alice2.mp3 (второе преобразование к этому моменту еще не закончилось и защита не снята) и еще пару файлов, к делу не относящихся.
FFmpeg при этом держит открытым Alice.wav (читает), temp4.mp3 (пишет), Alice1.mp3 и... еще пару файлов, к делу не относящихся - тех же, что и у Bard.
И тут до меня стало медленно доходить... я начал вспоминать, что, типа, при старте дочернего процесса открытые файлы, типа, наследуются (inherited). Но я специально возился с этим при создании pipe-ов - чтобы нужная pipe наследовалась, а ненужная - нет. И там в CreateNamedPipe, CreateFile и DuplicateHandle требовались специальные телодвижения, чтобы то, что нужно, отнаследовалось и попало в дочерний процесс.
А тут... я просто использовал обыкновенный fopen (ну ладно, не совсем fopen, но нечто очень близкое - _wsopen). Как оно могло отнаследоваться?
Продебажил... и что вы думаете? Оказывается реализация fopen в Microsoft Visual Studio такова, что она спЫцЫально делает все файлы наследуемыми. Каково?!
Разгадка была близка стоило лишь немного подумать.
Я запускал преобразования с интервалом в секунду-две. И происходило следующее:
Вот такие дела. А поиск решения стал почти что вторым детективом. Чтобы не писать совсем-многа-многа-буков, изложу его кратко.
1. Функциям _wsopen, fopen и подобным можно задать режим открытия с буковкой N, это приведет к созданию ненаследуемого файла. Например: fopen("Alice1.mp3", "rbN");
2. Еще можно явно перечислить в CreateProcess, какие файлы надо наследовать. Это делается через структуру STARTUPINFOEX. Но увы, увы, это не будет работать под Windows XP, а только начиная с Vista.
3. Если вы используете для досрочной терминации дочернего процесса имитацию Ctrl-C через вызов GenerateConsoleCtrlEvent, то рискуете вляпаться в ту же историю, но чуть-чуть на новый лад: если захотите защитить родительский процесс от самоубийства через вызов SetConsoleCtrlHandler(NULL, FALSE), то это тоже отнаследуется и защитит дочерний процесс, когда вы захотите запустить его повторно. Вот, кстати, самый правильный способ убиения дочернего консольного процесса из оконного родителя (проверку ошибок опускаю):
Иногда при кодировании случаются ситуёвины, напоминающие детектив. Вчера такое вот случилось.
Ковыряюсь я в своем звуковом редакторе "Bard". Как полигон для тренировки и освоения разных технологий - очень даже зачетная вещь. Вчера захотелось мне странного: чтобы два преобразования музыкальных файлов шли в параллель.
Преобразования у меня трех типов - built-in, на основе ACM и на основе любимой многими утилиты FFmpeg. Вот о последней и пойдет речь.
Я эту FFmpeg имел в самых разных позах - например, работал прилагаемой к ней с ней библиотекой кодеков через ейное API, собирал саму утилиту как модуль-часть-программы и как отдельную подгружаемую на runtime DLL-ку с кастрированием лишних функций... как-то все не то. И самый последний вид сношений, к которому я прибег, это использование pipe-ов.
Вкратце суть проблемы была такой: на вход одного преобразования подается файл Alice.wav, на выходе ождижается Alice1.mp3. Второе преобразование на вход берет тот же Alice.wav, на выходе - Alice2.mp3.
И вот... запускаю я два преобразования одновременно (с интервалом в пару секунд) и совершенно внезапно программа выдает ошибку: "доступ запрещен, файл: Alice1.mp3". Ну, я, естественно, лезу в стек, и обнаруживаю, что сбойнула функция rename. Кто бы мог подумать, какой длиннющий детектив породит такая ерунда.
Небольшое лирическое отступление. Правильные пацаны не подают на преобразователь выходной файл, т.к. преобразование может идти поверх прежней версии файла (с перезаписью), сбойнуть, и в результате получится полуперезаписанный файл - и не тот, что прежде, и не тот, что ожидается. Поэтому вместо прямой записи в Alice1.mp3 идет запись во временный файл, а потом этот временный подменяется через переименования. То есть, как-то вот так:
ffmpeg: Alice.wav -> temp1.mp3 rename Alice1.mp3 -> temp2.tmp rename temp1.mp3 -> Alice1.mp3 del temp2.tmp
Благодаря этой схеме, при ошибках и даже краше на любом этапе в живых останется либо прежняя версия файла, либо новая, либо обе. До последнего del возможен откат.
Ну так вот... на этой схеме виден тот самый сбойный "rename Alice1.mp3 -> temp2.tmp".
И начался детектив...
Первое предположение было - "сам дурак". Подозрение на модуль защиты. Я, видите ли, немножко параноик. И я баюс-баюс, что пока ffmpeg вдумчиво и печально ковыряет мегабайты, какая-нибудь сцукка-пользователь возьмет и что-нибудь сделает с Alice1.mp3. Например, возьмет и захочет проиграть старую версию в winamp-е или еще где, и когда преобразование завершится, файл окажется заблокирован на запись. Так что я в начале преобразования открываю этот файл с запретом записи. А потом, перед тем самым rename закрываю файл. Так вот заподозрил я, что файл не закрывается, не освобождается. Проверял и так, и эдак - все вроде бы нормально. Должен освободиться. Останавливал программу перед самым rename и переименовывал из консоли - все окей. То есть, файл оставался заблокированным всего пару секунд или меньше. Трехсекундный Sleep перед rename - и все начинает работать.
Вторым подозреваемым стал антивирус. Я подумал: а вдруг этой сцукке внезапно приспичило просканировать файл? Бывало уже такое. Отключение антивируса не помогло. Что за фигня?
И тут я вспомнил, что есть у меня качественная
Проблема только в том, что удержание файла длилось недолго, после чего переименование происходило спокойно. Как поймать момент?
Решение пришло в виде строчки кода:
WinExec("cmd /K c:\\Tools\\handle.exe Alice1.mp3");
Маленькая хитрость: "cmd /K " запускает утилиту handle в отдельной консоли и НЕ закрывает консоль по окончанию работы, так, чтобы я успел все спокойно посмотреть.
И вскрылась страшная тайна! (тревожная музыка)
ffmpeg.exe D:\GW_work\Alice1.mp3
То есть, файл держит утилита ffmpeg. Но как, Холмс?! Если посмотреть выше на схемку преобразований, ffmpeg вообще ничего не знает про Alice1.mp3, она пишет в другой, временный файл (что я немедленно проверил с помощью OutputDebugString).
Я еще попробовал запустить handle с опцией -p, посмотреть, какие файлы вообще держатся моими процессами в этот момент. И получилось, что:
Bard держит открытым Alice.wav (для защиты), Alice2.mp3 (второе преобразование к этому моменту еще не закончилось и защита не снята) и еще пару файлов, к делу не относящихся.
FFmpeg при этом держит открытым Alice.wav (читает), temp4.mp3 (пишет), Alice1.mp3 и... еще пару файлов, к делу не относящихся - тех же, что и у Bard.
И тут до меня стало медленно доходить... я начал вспоминать, что, типа, при старте дочернего процесса открытые файлы, типа, наследуются (inherited). Но я специально возился с этим при создании pipe-ов - чтобы нужная pipe наследовалась, а ненужная - нет. И там в CreateNamedPipe, CreateFile и DuplicateHandle требовались специальные телодвижения, чтобы то, что нужно, отнаследовалось и попало в дочерний процесс.
А тут... я просто использовал обыкновенный fopen (ну ладно, не совсем fopen, но нечто очень близкое - _wsopen). Как оно могло отнаследоваться?
Продебажил... и что вы думаете? Оказывается реализация fopen в Microsoft Visual Studio такова, что она спЫцЫально делает все файлы наследуемыми. Каково?!
Разгадка была близка стоило лишь немного подумать.
Я запускал преобразования с интервалом в секунду-две. И происходило следующее:
Bard: держит открытыми Alice.wav, Alice1.mp3, Alice2.mp3 ffmpeg (1-й экземпляр): Alice.wav -> temp1.mp3 запустился и унаследовал Alice.wav, Alice1.mp3, Alice2.mp3 ffmpeg (2-й экземпляр): Alice.wav -> temp4.mp3 запустился и унаследовал Alice.wav, Alice1.mp3, Alice2.mp3 ffmpeg (1-й экземпляр): кончил, отпустил все унаследованное ffmpeg (2-й экземпляр): еще пока работает Bard отпустил Alice1.mp3 Bard попытался переименовать Alice1.mp3 в temp2.tmp Но ffmpeg(2-й экземпляр) держит унаследованный Alice1.mp3!!! Бум-ц!
Вот такие дела. А поиск решения стал почти что вторым детективом. Чтобы не писать совсем-многа-многа-буков, изложу его кратко.
1. Функциям _wsopen, fopen и подобным можно задать режим открытия с буковкой N, это приведет к созданию ненаследуемого файла. Например: fopen("Alice1.mp3", "rbN");
2. Еще можно явно перечислить в CreateProcess, какие файлы надо наследовать. Это делается через структуру STARTUPINFOEX. Но увы, увы, это не будет работать под Windows XP, а только начиная с Vista.
3. Если вы используете для досрочной терминации дочернего процесса имитацию Ctrl-C через вызов GenerateConsoleCtrlEvent, то рискуете вляпаться в ту же историю, но чуть-чуть на новый лад: если захотите защитить родительский процесс от самоубийства через вызов SetConsoleCtrlHandler(NULL, FALSE), то это тоже отнаследуется и защитит дочерний процесс, когда вы захотите запустить его повторно. Вот, кстати, самый правильный способ убиения дочернего консольного процесса из оконного родителя (проверку ошибок опускаю):
static BOOL WINAPI HandlerRoutine(_In_ DWORD dwCtrlType) { if (dwCtrlType == CTRL_BREAK_EVENT) //именно CTRL_BREAK, а не CTRL_C! return TRUE; return FALSE; } .... mutex.lock(); AttachConsole(childProcessId); SetConsoleCtrlHandler(HandlerRoutine, TRUE); //но не SetConsoleCtrlHandler(NULL, FALSE)! GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, 0); FreeConsole(); mutex.unlock();
no subject
Date: 2013-03-06 08:58 pm (UTC)Под Юниксом этого бы не было, потому что Юникс пайпы отрабатывает корректнее...
no subject
Date: 2013-03-06 09:08 pm (UTC)Винда в данном случае не виновата, это реализация run-time библиотек в Visual Studio C++ оказалась слегка странноватой - ну на фига делать все файлы по-умолчанию наследуемыми?
В юниксе тоже все по-умолчанию наследуется, только там файлы, наверное, не заблокируются, благодаря самому факту наследования. С другой стороны, когда действительно надо будет заблокировать - начнутся мучительные поиски метода, который будет работать в разных версиях.
no subject
Date: 2013-03-06 11:35 pm (UTC)no subject
Date: 2013-03-07 05:28 am (UTC)The above record locks may be either advisory or mandatory, and are advisory by default. Advisory locks are not enforced and are useful only between cooperating processes.
no subject
Date: 2013-03-07 05:32 am (UTC)no subject
Date: 2013-03-07 05:48 am (UTC)То есть, отсутствует полезная функция. Что-то мне подсказывает, что линуксоиды скажут: это хорошо, это правильно, просто я не понимаю своего счастья :)
no subject
Date: 2013-03-07 05:56 am (UTC)no subject
Date: 2013-03-07 05:59 am (UTC)no subject
Date: 2013-03-09 04:27 am (UTC)А с промежуточными состояниями там где они возможны борятся просто - если нельзя (сложно) сделать файл с инкрементными изменениями - делается временный (который потом перемещается в постоянное место), а все временные файлы в одном общем мусорнике, обычно /tmp , и он конечно-же не бакапится, а в постоянном месте _всегда_ будет только нормальный файл.
no subject
Date: 2013-03-07 04:56 am (UTC)Их, правда, не всегда хватает.
no subject
Date: 2013-03-12 10:49 am (UTC)больше всего похоже на эмуляцию поведения fopen() из никсов. только не спрашивай, на кой эта эмуляция сдалась.
«заблокировать» же в никсах — задача нетривиальная.
no subject
Date: 2013-03-12 01:49 pm (UTC)no subject
Date: 2013-03-12 01:54 pm (UTC)no subject
Date: 2013-03-12 04:36 pm (UTC)no subject
Date: 2013-03-12 10:46 am (UTC)no subject
Date: 2013-03-19 12:32 pm (UTC)Извините меня пожалуйста. Ржал как конь с формулировки "Версия программы, выложенная на сайте автора, проверена на отсутствие вирусов." Жаль что о ее результате ни слова =)
Не совсем понятно наличие последовательности из 72-УХ тегов
Application use external libraries which are freeware too. License information and
source code is available at developer sites: library zlib (http://zlib.net, license: zlib license)
and library FFmpeg (http://ffmpeg.org, license: GNU LGPL).
..
no subject
Date: 2013-03-19 03:26 pm (UTC):)
[ Не совсем понятно наличие последовательности из 72-УХ тегов ]
Чтобы при переходе на <a name=...> прокручивалось до начала <a name=...>
no subject
Date: 2013-03-19 03:29 pm (UTC)Смущает ссылка на русскую версию находящаяся в 2 строках от этого текста =) , неужто кто-то умудриться не найти начало русского текста.
Кстати интересное у вас уличение и из той же оперы мб вам будет интересно поучаствовать в блендере, тоже опенсорс? Или просто узнать немного о CG, жутко-интересная тема.