Используя события файловой системы API
События Файловой системы API состоят из нескольких отличных групп функций. Можно получить общую информацию об объемах и событиях при помощи начинающихся функций FSEvents
. Можно создать новый поток событий, выполнить операции на потоке, и т.д. с помощью начинающихся функций FSEventStream
.
Жизненный цикл потока событий файловой системы следующие:
Приложение создает поток путем вызова
FSEventStreamCreate
илиFSEventStreamCreateRelativeToDevice
.Приложение планирует поток на цикл выполнения путем вызова
FSEventStreamScheduleWithRunLoop
.Приложение говорит демону событий файловой системы начинать отправлять события путем вызова
FSEventStreamStart
.События прикладных служб, когда они поступают. API отправляет события путем вызова функции обратного вызова, указанной на шаге 1.
Приложение говорит демону прекращать отправлять события путем вызова
FSEventStreamStop
.Если приложение должно перезапустить поток, перейдите к шагу 3.
Приложение не планирует событие от своего цикла выполнения путем вызова
FSEventStreamUnscheduleFromRunLoop
.Приложение лишает законной силы поток путем вызова
FSEventStreamInvalidate
.Приложение выпускает свою ссылку на поток путем вызова
FSEventStreamRelease
.
Эти шаги объяснены более подробно в следующих разделах.
Добавление включает директивы
Перед использованием потока событий файловой системы API необходимо включать платформу Core Services следующим образом:
#include <CoreServices/CoreServices.h> |
Когда Вы компилируете, необходимо включать Платформу Core Services путем добавления его к цели в XCode или путем добавления флага -framework CoreServices
Вашему компоновщику отмечает на командной строке или в Make-файле.
Создание потока событий
События API файловой системы поддерживают два типа потоков событий: потоки событий на диск и потоки событий на узел. Прежде чем можно будет создать поток, необходимо решить который тип потока создать: поток событий на узел или поток событий на диск. Можно создать эти потоки путем вызывания функций FSEventStreamCreate
и FSEventStreamCreateRelativeToDevice
, соответственно.
Поток событий на узел состоит из событий, IDs которых увеличивается относительно других событий на том узле. Этот IDs, как гарантируют, будет уникален за одним исключением: если дополнительные диски добавляются от другого компьютера, также выполнявшего OS X v10.5 или позже, исторический IDs может конфликтовать между этими объемами. Любые новые события автоматически запустятся после исторического ID с самым высоким номером для любого присоединенного диска.
Поток событий на диск, в отличие от этого, состоит из событий, IDs которых увеличивается относительно предыдущих событий на том диске. Это не имеет никакого отношения с другими событиями на других дисках, и таким образом необходимо создать отдельный поток событий для каждого физического устройства, которое Вы хотите контролировать.
В целом, если Вы пишете программное обеспечение, требующее персистентности, необходимо использовать потоки на диск для предотвращения любого беспорядка вследствие конфликтов ID. В отличие от этого, если Вы контролируете для изменений в каталоге или дереве каталогов во время нормального выполнения, таких как наблюдение каталога очереди, потоки на узел являются самыми удобными.
При контроле файлов в корневой файловой системе любой потоковый механизм будет вести себя так же.
Например, следующий отрывок показывает, как создать поток событий:
/* Define variables and create a CFArray object containing |
CFString objects containing paths to watch. |
*/ |
CFStringRef mypath = CFSTR("/path/to/scan"); |
CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void **)&mypath, 1, NULL); |
void *callbackInfo = NULL; // could put stream-specific data here. |
FSEventStreamRef stream; |
CFAbsoluteTime latency = 3.0; /* Latency in seconds */ |
/* Create the stream, passing in a callback */ |
stream = FSEventStreamCreate(NULL, |
&myCallbackFunction, |
callbackInfo, |
pathsToWatch, |
kFSEventStreamEventIdSinceNow, /* Or a previous event ID */ |
latency, |
kFSEventStreamCreateFlagNone /* Flags explained in reference */ |
); |
Как только Вы создали поток событий, необходимо запланировать его на цикл выполнения приложения. Чтобы сделать это, вызвать FSEventStreamScheduleWithRunLoop
, передавая в недавно создаваемом потоке, ссылке на Ваш цикл выполнения и режиме цикла выполнения. Для получения дополнительной информации о выполненных циклах, считайте Циклы Выполнения.
Если у Вас уже не будет цикла выполнения, то необходимо будет посвятить поток этой задаче. После создания потока с помощью предпочтительного API вызвать CFRunLoopGetCurrent
выделить начальный выполненный цикл для того потока. Любые будущие вызовы к CFRunLoopGetCurrent
возвратит тот же цикл выполнения.
Например, следующий отрывок показывает, как запланировать поток, вызванный stream
, на цикле выполнения текущего потока (еще не работающий):
FSEventStreamRef stream; |
/* Create the stream before calling this. */ |
FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); |
Последний шаг в установке потока событий должен вызвать FSEventStreamStart
. Эта функция говорит потоку событий начинать отправлять события. Собственный параметр является потоком событий для запуска.
Как только поток событий был создан и запланирован, если Ваш цикл выполнения уже не работает, необходимо запустить его путем вызова CFRunLoopRun
.
Обработка событий
Ваш обратный вызов обработчика событий должен соответствовать прототипу для FSEventStreamCallback
. Параметры описаны в справочной документации для FSEventStreamCallback
тип данных.
Ваш обработчик событий получает три списка: список путей, список идентификаторов и список флагов. В действительности они представляют список событий. Первое событие состоит из первой записи, взятой от каждого из массивов и т.д. Ваш обработчик должен выполнить итерации через эти списки, обработав события по мере необходимости.
Для каждого события необходимо отсканировать каталог в указанном пути, обработав его содержание, как желаемый. Обычно, необходимо отсканировать только точный каталог, указанный путем. Однако существует три ситуации в который дело обстоит не так:
Если событие в каталоге имеет место в приблизительно то же время как одно или более событий в подкаталоге того каталога, события могут быть объединены в единственное событие. В этом случае Вы получите событие с
kFSEventStreamEventFlagMustScanSubDirs
флаг установлен. При получении такого события необходимо рекурсивно повторно отсканировать путь, перечисленный в конечном счете. Дополнительные изменения находятся не обязательно в непосредственном дочернем элементе перечисленного пути.Если ошибка связи происходит между ядром и демоном пространства пользователя, можно получить событие с любым
kFSEventStreamEventFlagKernelDropped
илиkFSEventStreamEventFlagUserDropped
флаг установлен. В любом случае необходимо сделать полное сканирование любых каталогов, которые Вы контролируете, потому что нет никакого способа определить то, что, возможно, изменилось.Если корневой каталог, который Вы наблюдаете, удален, перемещен или переименован (или если какой-либо из его родительских каталогов перемещен или переименован), каталог может прекратить существование. Если Вы заботитесь об этом, необходимо передать флаг
kFSEventStreamCreateFlagWatchRoot
при создании потока. В этом случае Вы получите событие с флагомkFSEventStreamEventFlagRootChanged
и идентификатор события нуля (0
). В этом случае необходимо повторно отсканировать весь каталог, потому что он может не существовать.Если необходимо выяснить, куда каталог переместился, необходимо открыть корневой каталог с
open
, тогда передачаF_GETPATH
кfcntl
найти его текущий путь. См. страницу руководства дляfcntl
для получения дополнительной информации.Если число подходов событий 2^64, повторится идентификатор события. Когда это произойдет, Вы получите событие с флагом
kFSEventStreamEventFlagEventIdsWrapped
. К счастью, по крайней мере в ближайшем времени, это вряд ли произойдет на практике, поскольку 64 бита предусматривают достаточно пространства приблизительно для одного события на область размера средства стирания на поверхности Земли (включая воду) и потребовали бы, чтобы приблизительно 2 000 эксабайт (2 миллиона миллионов гигабайтов) хранения содержали их всех. Однако необходимо все еще проверить на этот флаг и принять соответствующие меры при получении его.
Как часть Вашего обработчика, Вы, возможно, иногда должны получить список путей, наблюдаемых потоком текущего события. Можно получить тот список путем вызова FSEventStreamCopyPathsBeingWatched
.
Иногда, можно хотеть контролировать, где Вы находитесь в потоке. Если Ваш код уменьшается значительно позади, Вы могли бы, например, принять решение сделать меньше обработки. Можно узнать последнее событие, включенное в текущий пакет событий путем вызова FSEventStreamGetLatestEventId
(или путем исследования последнего события в списке). Можно тогда сравнить это со значением, возвращенным FSEventsGetCurrentEventId
, который возвращает самое высокое пронумерованное событие в системе.
Например, следующий фрагмент кода показывает очень простой обработчик.
void mycallback( |
ConstFSEventStreamRef streamRef, |
void *clientCallBackInfo, |
size_t numEvents, |
void *eventPaths, |
const FSEventStreamEventFlags eventFlags[], |
const FSEventStreamEventId eventIds[]) |
{ |
int i; |
char **paths = eventPaths; |
// printf("Callback called\n"); |
for (i=0; i<numEvents; i++) { |
int count; |
/* flags are unsigned long, IDs are uint64_t */ |
printf("Change %llu in %s, flags %lu\n", eventIds[i], paths[i], eventFlags[i]); |
} |
} |
Используя персистентные события
Одной из наиболее мощных функций событий файловой системы является их персистентность через перезагрузки. Это означает, что Ваше приложение может легко узнать то, что произошло с определенного времени или определенного события в удаленном прошлом. Путем выполнения так, можно узнать, какие файлы были изменены, даже когда не работает приложение. Это может значительно упростить задачи, такие как поддержка измененных файлов, проверка измененные зависимости в многофайловых проектах, и т.д.
Для работы с персистентными событиями приложение должно регулярно хранить последний идентификатор события, который оно обрабатывает. Затем когда это должно возвратиться и видеть то, что изменили файлы, это только должно смотреть на события, произошедшие после последнего известного события. Для получения всех событий начиная с определенного события в прошлом, Вы передаете идентификатор события в sinceWhen
параметр FSEventStreamCreate
или FSEventStreamCreateRelativeToDevice
.] [На основе для каждого устройства, можно также легко использовать метку времени для определения который события включать. Чтобы сделать это, необходимо сначала вызвать FSEventsGetLastEventIdForDeviceBeforeTime
получить последний идентификатор события sinceWhen
параметр FSEventStreamCreateRelativeToDevice
.
На основе для каждого устройства можно также легко использовать метку времени для определения который события включать. Чтобы сделать это, необходимо сначала вызвать FSEventsGetLastEventIdForDeviceBeforeTime
получить последний идентификатор события для того устройства до штампа требуемого времени. Вы тогда передаете получающееся значение FSEventStreamCreateRelativeToDevice
. Это описано далее в Специальных замечаниях для Потоков Для каждого устройства.
При работе с персистентными событиями обычно используемый метод должен объединить уведомления о событии файловой системы с кэшируемым «снимком» метаданных файлов в дереве. Этот процесс описан далее в Создании Снимка Иерархии каталогов.
Создание снимка иерархии каталогов
События файловой системы говорят Вам что что-то в данном измененном каталоге. В некоторых случаях это достаточно — например, если Ваше приложение является печатью или почтовым спулером, все, что это должно знать, то, что файл был добавлен к каталогу.
В некоторых случаях, однако, это недостаточно, и необходимо знать точно, что изменилось в каталоге. Самый простой способ решить эту проблему состоит в том, чтобы взять иерархию каталогов снимка, храня Вашу собственную копию состояния системы в данный момент времени. Вы могли бы, например, сохранить список имен файлов и продержаться измененные даты, таким образом позволив Вам определить, какие файлы были изменены с прошлого раза Вы выполнили резервное копирование.
Вы делаете это путем итерации через иерархию и роста структуры данных по Вашему выбору. Поскольку Вы кэшируете эти метаданные, если Вы видите изменения во время процесса кэширования, можно перечитать каталог или каталоги, изменившиеся для получения обновленного снимка. Как только у Вас есть кэшируемое дерево метаданных, точно отражающих текущее состояние иерархии, в которой Вы обеспокоены, можно тогда определить то, что файл или файлы, измененные в каталоге или иерархии (после уведомления о событии файловой системы) путем сравнения текущего каталога, утверждают со снимком.
OS X обеспечивает много APIs, который может сделать это проще. scandir
функционируйте возвращает массив записей каталога, которых можно быстро выполнить итерации через. Это несколько проще, чем чтение каталога вручную с opendir
, readdir
, и т.д., и немного более эффективно, так как Вы будете всегда выполнять итерации через весь каталог при кэшировании так или иначе.
Функции двоичного дерева tsearch
, tfind
, twalk
, и tdelete
может упростить работу с большими деревьями поиска. В частности двоичные деревья являются простым способом быстрого нахождения кэшируемой информации о файле из определенного каталога. Следующий фрагмент кода демонстрирует надлежащий способ вызвать эти функции:
Перечисление 2-1 Используя tsearch, tfind, twalk, и tdelete API.
#include <unistd.h> |
#include <stdlib.h> |
#include <stdio.h> |
#include <dirent.h> |
#include <sys/stat.h> |
#include <string.h> |
#include <search.h> |
int array[] = { 1, 17, 2432, 645, 2456, 1234, 6543, 214, 3, 45, 34 }; |
void *dirtree; |
static int cmp(const void *a, const void *b) { |
if (*(int *)a < *(int *)b) return -1; |
if (*(int *)a > *(int *)b) return 1; |
return 0; |
} |
void printtree(void); |
/* Pass in a directory as an argument. */ |
int main(int argc, char *argv[]) |
{ |
int i; |
for (i=0; i< sizeof(array) / sizeof(array[0]); i++) { |
void *x = tsearch(&array[i], &dirtree, &cmp); |
printf("Inserted %p\n", x); |
} |
printtree(); |
void *deleted_node = tdelete(&array[2], &dirtree, &cmp); |
printf("Deleted node %p with value %d (parent node contains %d)\n", |
deleted_node, array[2], **(int**)deleted_node); |
for (i=0; i< sizeof(array) / sizeof(array[0]); i++) { |
void *node = tfind(&array[i], &dirtree, &cmp); |
if (node) { |
int **x = node; |
printf("Found %d (%d) at %p\n", array[i], **x, node); |
} else { |
printf("Not found: %d\n", array[i]); |
} |
} |
exit(0); |
} |
static void printme(const void *node, VISIT v, int k) |
{ |
const void *myvoid = *(void **)node; |
const int *myint = (const int *)myvoid; |
// printf("x\n"); |
if (v != postorder && v != leaf) return; |
printf("%d\n", *myint); |
} |
void printtree(void) |
{ |
twalk(dirtree, &printme); |
} |
Два необычных проектных решения в этом API могут сделать его хитрым для использования правильно, если Вы не использовали его прежде в других основанных на UNIX или подобных UNIX операционных системах:
tsearch
иtdelete
функции берут адрес древовидной переменной, не самой древовидной переменной. Это вызвано тем, что они должны изменить значение, сохраненное в древовидной переменной, когда они создают или удаляют начальный корневой узел, соответственно.Даже при том, что
tfind
не изменяет значение корня, оно все еще берет адрес корня как его параметр, не сам корневой указатель. Частая ошибка состоит в том, чтобы передать вdirtree
указатель. Фактически, необходимо передать в&dirtree
(адресdirtree
указатель).Значения передали обратному вызову
twalk
и значения, возвращенныеtfind
иtsearch
адрес, где указатель на данные сохранен, не само значение данных. Поскольку этот код передал в адресе целого числа, необходимо разыменовать то значение дважды — один раз для оригиналаaddress-of
оператор и один раз разыменовать указатель на тот указатель, который возвращают эти функции.В отличие от других функций, однако, функции
tdelete
не возвращает адрес в дереве, где хранятся данные. Это вызвано тем, что данные больше не хранятся в дереве. Вместо этого это возвращает родительский узел узла, который это удалило.
Функции POSIX stat
и lstat
обеспечьте легкий доступ к метаданным файла. Эти две функции отличаются по своей обработке символьных ссылок. lstat
функция предоставляет информацию о самой ссылке, в то время как stat
функция предоставляет информацию о файле, на который указывает ссылка. Вообще говоря, при работе с уведомлениями о событии файловой системы, Вы, вероятно, захотите использовать lstat
, потому что изменения в базовом файле не приведут к уведомлению изменения для каталога, содержащего символьную ссылку на тот файл. Однако, если Вы работаете с управляемой файловой структурой, в которой символьные ссылки всегда указывают в Вашем наблюдаемом дереве, у Вас могла бы быть причина использовать stat
.
Для примера инструмента, создающего снимок каталога, посмотрите пример кода Наблюдателя.
Очистка
Когда Вам больше не нужен поток событий файловой системы, необходимо всегда убирать против течения, чтобы избежать пропускать память и дескрипторы. Перед очисткой, однако, необходимо сначала остановить цикл выполнения путем вызова FSEventStreamStop
.
Затем, необходимо вызвать FSEventStreamInvalidate
. Эта функция не планирует поток от всех циклов выполнения с единственным вызовом. Если необходимо не запланировать его от только единственного цикла выполнения, или если необходимо переместить поток событий между двумя выполненными циклами, необходимо вместо этого вызвать FSEventStreamUnscheduleFromRunLoop
. Можно тогда перепланировать поток событий при желании путем вызова FSEventStreamScheduleWithRunLoop
.
Как только Вы лишили законной силы поток событий, можно выпустить его путем вызова FSEventStreamRelease
. Когда потоковый выпуск и поток сохраняют баланс количеств и больше нет никаких происшествий сохраняемого потока, поток будет освобожден.
Существует три других связанных с очисткой функции, о которых необходимо знать при определенных обстоятельствах. Если Ваше приложение должно удостовериться, что файловая система достигла устойчивого состояния до очистки против течения, можно счесть полезным сбросить поток. Можно сделать это с одной из двух функций: FSEventStreamFlushAsync
и FSEventStreamFlushSync
.
При сбрасывании событий не возвратится синхронный вызов, пока все незаконченные события не сбрасываются. Асинхронный вызов сразу возвратится и возвратит идентификатор события (типа FSEventStreamEventId
) из последнего ожидания события. Когда последнее событие было обработано, при желании, можно тогда использовать это значение в функции обратного вызова для определения.
Заключительная функция, связанная с очисткой, FSEventsPurgeEventsForDeviceUpToEventId
. Эта функция может только быть вызвана полностью пользователь, потому что она уничтожает архивную запись событий на объеме до данного идентификатора события. Как правило Вы никогда не должны вызывать эту функцию, потому что Вы не можете безопасно предположить, что Ваше приложение является единственным потребителем данных о событии.
Если Вы пишете специализированное приложение (решение для резервного копирования предприятия, например), может быть надлежащим вызвать эту функцию для обрезки записи события на некоторый разумный размер, чтобы препятствовать тому, чтобы он стал произвольно большим. Необходимо сделать это, только если администратор явно запрашивает это поведение, однако, и необходимо всегда просить подтверждение (или прежде, чем выполнить работу или прежде, чем включить любое правило, которое заставило бы его выполняться в более позднее время).
Специальные замечания для потоков для каждого устройства
В дополнение к соображениям, описанным в Обработке Событий, потоки, создаваемые с FSEventStreamCreateRelativeToDevice
, потоки для каждого устройства имеют некоторые специальные характеристики, о которых необходимо знать:
Все пути относительно корня объема, который Вы контролируете, не относительно системного корня. Это применяется к и пути, используемому при создании потока и к любому пути, который обратный вызов получает как часть события.
Устройство IDs может не остаться тем же через перезагрузки (особенно со съемными устройствами). Это - Ваша ответственность гарантировать, что объем, на который Вы смотрите, является правильным путем сравнения UUID.
В дополнение к функциям предусмотрел потоки в масштабе всей системы, можно получить UUID для устройства, связанного с потоком путем вызова FSEventStreamGetDeviceBeingWatched
.
Можно получить уникальный идентификатор для устройства путем вызова FSEventsCopyUUIDForDevice
. Если этот уникальный идентификатор отличается, чем тот, полученный из предыдущего выполнения, это может означать много вещей. Это могло означать, что у пользователя есть два объема с тем же именем, что пользователь переформатировал объем с тем же именем, или что идентификаторы события были очищены для объема. В любом из этих случаев любые предыдущие события для объема не применяются к этому определенному объему, но они могут все еще быть допустимыми для другого объема.
Если Вы находите, что UUID для объема соответствует то, что было сохранено на предыдущем выполнении, но идентификатор события ниже, чем последняя версия, которую Вы сохранили, это может означать, что пользователь восстановил объем от резервного копирования, или это может означать, что IDs повторился или был очищен. В любом случае любые сохраненные события, которые Вы можете иметь для устройства, недопустимы.
Наконец при использовании персистентных событий можно также использовать функцию FSEventsGetLastEventIdForDeviceBeforeTime
найти последнее событие до метки времени. Этот идентификатор события является персистентным, и может быть особенно полезным для выполнения инкрементных резервных копий.
Используемый формат времени является a CFAbsoluteTime
значение, измеряющееся в секундах с 1 января 2001. Для других форматов метки времени необходимо преобразовать их в этот формат следующим образом:
Если Вы пишете приложение Какао, необходимо использовать
NSDate
объект выполнить любые преобразования, затем используйтеCFDateGetAbsoluteTime
получить соответствиеCFAbsoluteTime
значение. (Можно прозрачно передатьNSDate
возразите как aCFDateRef
.)Если Вы начиная с метки времени POSIX в приложении некакао, необходимо вычесть
kCFAbsoluteTimeIntervalSince1970
от значения для преобразования в aCFAbsoluteTime
значение. Обязательно всегда используйте метки времени на основе GMT.Если бы Вы работаете с устаревшей меткой времени Углерода в приложении некакао, Вы вычли бы
kCFAbsoluteTimeIntervalSince1904
. Обязательно всегда используйте метки времени на основе GMT.
Для получения дополнительной информации о типах даты и времени, необходимо считать Руководство по программированию Даты и времени для Базовой Основы.