psilogic: (croco)
[personal profile] psilogic
Пердуперждение: эта запись вряд ли будет понятна непрограммистам. А на программистов может навеять скуку... или не навеять.

Иногда при кодировании случаются ситуёвины, напоминающие детектив. Вчера такое вот случилось.

Ковыряюсь я в своем звуковом редакторе "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 - и все начинает работать.

Вторым подозреваемым стал антивирус. Я подумал: а вдруг этой сцукке внезапно приспичило просканировать файл? Бывало уже такое. Отключение антивируса не помогло. Что за фигня?

И тут я вспомнил, что есть у меня качественная трава тулза от товарища Марка Руссиновича, назывется handle. Эта лапочка может сказать мне, какая гадюка держит файл.

Проблема только в том, что удержание файла длилось недолго, после чего переименование происходило спокойно. Как поймать момент?

Решение пришло в виде строчки кода:

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();




Date: 2013-03-06 08:58 pm (UTC)
From: [identity profile] pascendi.livejournal.com
Винда...
Под Юниксом этого бы не было, потому что Юникс пайпы отрабатывает корректнее...

Date: 2013-03-06 09:08 pm (UTC)
From: [identity profile] psilogic.livejournal.com
А что некорректного в том, как отрабатывает винда?

Винда в данном случае не виновата, это реализация run-time библиотек в Visual Studio C++ оказалась слегка странноватой - ну на фига делать все файлы по-умолчанию наследуемыми?

В юниксе тоже все по-умолчанию наследуется, только там файлы, наверное, не заблокируются, благодаря самому факту наследования. С другой стороны, когда действительно надо будет заблокировать - начнутся мучительные поиски метода, который будет работать в разных версиях.

Date: 2013-03-06 11:35 pm (UTC)
From: [identity profile] lionet.livejournal.com
btw, искать метод не надо, fcntl(F_SETLK) наиболее переносим.

Date: 2013-03-07 05:28 am (UTC)
From: [identity profile] psilogic.livejournal.com
Вспомнил, что уже смотрел эту функцию. Меня тогда очень смутило вот это:


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.

Date: 2013-03-07 05:32 am (UTC)
From: [identity profile] lionet.livejournal.com
В Unix нет mandatory locking, fyi. То, что в линуксе так написано — оно нужно для того, чтобы SMB эмулировать, и на практике не работает.

Date: 2013-03-07 05:48 am (UTC)
From: [identity profile] psilogic.livejournal.com
[ В Unix нет mandatory locking, fyi. ]

То есть, отсутствует полезная функция. Что-то мне подсказывает, что линуксоиды скажут: это хорошо, это правильно, просто я не понимаю своего счастья :)

Date: 2013-03-07 05:56 am (UTC)
From: [identity profile] lionet.livejournal.com
Ну как бы да. Иначе бэкап просто так не сделаешь на загруженном сервере ;)

Date: 2013-03-07 05:59 am (UTC)
From: [identity profile] psilogic.livejournal.com
Если файл даже на чтение заблокирован, то скорее всего его содержимое в данный момент меняется, находится в промежуточном невалидном состоянии, и какой смысл бекапить мусор?

Date: 2013-03-09 04:27 am (UTC)
From: [identity profile] zyxman.livejournal.com
В Unix всего-лишь нет залочки по-умолчанию, а вообще она есть. То есть если сильно надо то конечно лочат.
А с промежуточными состояниями там где они возможны борятся просто - если нельзя (сложно) сделать файл с инкрементными изменениями - делается временный (который потом перемещается в постоянное место), а все временные файлы в одном общем мусорнике, обычно /tmp , и он конечно-же не бакапится, а в постоянном месте _всегда_ будет только нормальный файл.

Date: 2013-03-07 04:56 am (UTC)
From: [identity profile] pascendi.livejournal.com
Насчет мучительных поисков метода, работающего в разных версиях -- это да, Вы правы. Если не использовать самые простые, имеющиеся в базовых библиотеках. :-)
Их, правда, не всегда хватает.
Edited Date: 2013-03-07 04:58 am (UTC)

Date: 2013-03-12 10:49 am (UTC)
From: [personal profile] no1u1w1w6c
>на фига делать все файлы по-умолчанию наследуемыми?
больше всего похоже на эмуляцию поведения fopen() из никсов. только не спрашивай, на кой эта эмуляция сдалась.

«заблокировать» же в никсах — задача нетривиальная.

Date: 2013-03-12 01:49 pm (UTC)
From: [identity profile] psilogic.livejournal.com
Непродумано, ога. Сначала одну фичу (возможность переименовать открытый кем-то файл) не эмулируют, потом другую - эмулируют.

Date: 2013-03-12 01:54 pm (UTC)
From: [personal profile] no1u1w1w6c
переименовать можно, кстати — но только открытый на чтение. таким образом у меня .exe себя автоапдейтят. %-)

Date: 2013-03-12 04:36 pm (UTC)
From: [identity profile] psilogic.livejournal.com
интересная мысль - надо будет попробовать :)

Date: 2013-03-12 10:46 am (UTC)
From: [personal profile] no1u1w1w6c
гыг. форка нет, а эта фигня есть. фообще, fopen() в пингвинусе так же делает, но это потому, что fork() вообще наследует всё, что ни попадя. впрочем, переименовать или удалить всё равно можно что угодно. и что приятно — переименование является атомной операцией и стирает файл, если такой уже был. что позволяет избавиться от дырки между двумя rename и гарантировано удалить то, что не надо.

Date: 2013-03-19 12:32 pm (UTC)
From: [identity profile] m-eclipse.livejournal.com
Пока еще не все дочитал, но первая аналогия что приходит в голову кот Шредингера совершает суицид. Так как кот находиться сразу в 2 местах и при этом одновременно может существовать только один кот, вывод во втором месте находится копия первого кота и при попытки суицида не понятно что происходит парное самоубийство или перекрестное двойное убийство, хотя во втором случае один из котов может выжить =) хм мб это похоже на запутывание суицида кота Шредингера.

Извините меня пожалуйста. Ржал как конь с формулировки "Версия программы, выложенная на сайте автора, проверена на отсутствие вирусов." Жаль что о ее результате ни слова =)

Не совсем понятно наличие последовательности из 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).






..




Edited Date: 2013-03-19 02:06 pm (UTC)

Date: 2013-03-19 03:26 pm (UTC)
From: [identity profile] psilogic.livejournal.com
[ Жаль что о ее результате ни слова =) ]

:)

[ Не совсем понятно наличие последовательности из 72-УХ тегов ]

Чтобы при переходе на <a name=...> прокручивалось до начала <a name=...>
Edited Date: 2013-03-19 03:27 pm (UTC)

Date: 2013-03-19 03:29 pm (UTC)
From: [identity profile] m-eclipse.livejournal.com
Ого как коварно. Длинновата у вас плазма =)
Смущает ссылка на русскую версию находящаяся в 2 строках от этого текста =) , неужто кто-то умудриться не найти начало русского текста.

Кстати интересное у вас уличение и из той же оперы мб вам будет интересно поучаствовать в блендере, тоже опенсорс? Или просто узнать немного о CG, жутко-интересная тема.
Edited Date: 2013-03-19 03:45 pm (UTC)
Page generated Aug. 14th, 2025 05:52 pm
Powered by Dreamwidth Studios