Используя события файловой системы API

События Файловой системы API состоят из нескольких отличных групп функций. Можно получить общую информацию об объемах и событиях при помощи начинающихся функций FSEvents. Можно создать новый поток событий, выполнить операции на потоке, и т.д. с помощью начинающихся функций FSEventStream.

Жизненный цикл потока событий файловой системы следующие:

  1. Приложение создает поток путем вызова FSEventStreamCreate или FSEventStreamCreateRelativeToDevice.

  2. Приложение планирует поток на цикл выполнения путем вызова FSEventStreamScheduleWithRunLoop.

  3. Приложение говорит демону событий файловой системы начинать отправлять события путем вызова FSEventStreamStart.

  4. События прикладных служб, когда они поступают. API отправляет события путем вызова функции обратного вызова, указанной на шаге 1.

  5. Приложение говорит демону прекращать отправлять события путем вызова FSEventStreamStop.

  6. Если приложение должно перезапустить поток, перейдите к шагу 3.

  7. Приложение не планирует событие от своего цикла выполнения путем вызова FSEventStreamUnscheduleFromRunLoop.

  8. Приложение лишает законной силы поток путем вызова FSEventStreamInvalidate.

  9. Приложение выпускает свою ссылку на поток путем вызова 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 тип данных.

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

Для каждого события необходимо отсканировать каталог в указанном пути, обработав его содержание, как желаемый. Обычно, необходимо отсканировать только точный каталог, указанный путем. Однако существует три ситуации в который дело обстоит не так:

Как часть Вашего обработчика, Вы, возможно, иногда должны получить список путей, наблюдаемых потоком текущего события. Можно получить тот список путем вызова 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 операционных системах:

Функции POSIX stat и lstat обеспечьте легкий доступ к метаданным файла. Эти две функции отличаются по своей обработке символьных ссылок. lstat функция предоставляет информацию о самой ссылке, в то время как stat функция предоставляет информацию о файле, на который указывает ссылка. Вообще говоря, при работе с уведомлениями о событии файловой системы, Вы, вероятно, захотите использовать lstat, потому что изменения в базовом файле не приведут к уведомлению изменения для каталога, содержащего символьную ссылку на тот файл. Однако, если Вы работаете с управляемой файловой структурой, в которой символьные ссылки всегда указывают в Вашем наблюдаемом дереве, у Вас могла бы быть причина использовать stat.

Для примера инструмента, создающего снимок каталога, посмотрите пример кода Наблюдателя.

Очистка

Когда Вам больше не нужен поток событий файловой системы, необходимо всегда убирать против течения, чтобы избежать пропускать память и дескрипторы. Перед очисткой, однако, необходимо сначала остановить цикл выполнения путем вызова FSEventStreamStop.

Затем, необходимо вызвать FSEventStreamInvalidate. Эта функция не планирует поток от всех циклов выполнения с единственным вызовом. Если необходимо не запланировать его от только единственного цикла выполнения, или если необходимо переместить поток событий между двумя выполненными циклами, необходимо вместо этого вызвать FSEventStreamUnscheduleFromRunLoop. Можно тогда перепланировать поток событий при желании путем вызова FSEventStreamScheduleWithRunLoop.

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

Существует три других связанных с очисткой функции, о которых необходимо знать при определенных обстоятельствах. Если Ваше приложение должно удостовериться, что файловая система достигла устойчивого состояния до очистки против течения, можно счесть полезным сбросить поток. Можно сделать это с одной из двух функций: FSEventStreamFlushAsync и FSEventStreamFlushSync.

При сбрасывании событий не возвратится синхронный вызов, пока все незаконченные события не сбрасываются. Асинхронный вызов сразу возвратится и возвратит идентификатор события (типа FSEventStreamEventId) из последнего ожидания события. Когда последнее событие было обработано, при желании, можно тогда использовать это значение в функции обратного вызова для определения.

Заключительная функция, связанная с очисткой, FSEventsPurgeEventsForDeviceUpToEventId. Эта функция может только быть вызвана полностью пользователь, потому что она уничтожает архивную запись событий на объеме до данного идентификатора события. Как правило Вы никогда не должны вызывать эту функцию, потому что Вы не можете безопасно предположить, что Ваше приложение является единственным потребителем данных о событии.

Если Вы пишете специализированное приложение (решение для резервного копирования предприятия, например), может быть надлежащим вызвать эту функцию для обрезки записи события на некоторый разумный размер, чтобы препятствовать тому, чтобы он стал произвольно большим. Необходимо сделать это, только если администратор явно запрашивает это поведение, однако, и необходимо всегда просить подтверждение (или прежде, чем выполнить работу или прежде, чем включить любое правило, которое заставило бы его выполняться в более позднее время).

Специальные замечания для потоков для каждого устройства

В дополнение к соображениям, описанным в Обработке Событий, потоки, создаваемые с FSEventStreamCreateRelativeToDevice, потоки для каждого устройства имеют некоторые специальные характеристики, о которых необходимо знать:

В дополнение к функциям предусмотрел потоки в масштабе всей системы, можно получить UUID для устройства, связанного с потоком путем вызова FSEventStreamGetDeviceBeingWatched.

Можно получить уникальный идентификатор для устройства путем вызова FSEventsCopyUUIDForDevice. Если этот уникальный идентификатор отличается, чем тот, полученный из предыдущего выполнения, это может означать много вещей. Это могло означать, что у пользователя есть два объема с тем же именем, что пользователь переформатировал объем с тем же именем, или что идентификаторы события были очищены для объема. В любом из этих случаев любые предыдущие события для объема не применяются к этому определенному объему, но они могут все еще быть допустимыми для другого объема.

Если Вы находите, что UUID для объема соответствует то, что было сохранено на предыдущем выполнении, но идентификатор события ниже, чем последняя версия, которую Вы сохранили, это может означать, что пользователь восстановил объем от резервного копирования, или это может означать, что IDs повторился или был очищен. В любом случае любые сохраненные события, которые Вы можете иметь для устройства, недопустимы.

Наконец при использовании персистентных событий можно также использовать функцию FSEventsGetLastEventIdForDeviceBeforeTime найти последнее событие до метки времени. Этот идентификатор события является персистентным, и может быть особенно полезным для выполнения инкрементных резервных копий.

Используемый формат времени является a CFAbsoluteTime значение, измеряющееся в секундах с 1 января 2001. Для других форматов метки времени необходимо преобразовать их в этот формат следующим образом:

Для получения дополнительной информации о типах даты и времени, необходимо считать Руководство по программированию Даты и времени для Базовой Основы.