Using the File System Events API


파일 시스템 이벤트(File System Event) API 는 함수에 따라 크게 몇 개의 그룹으로 되어 있습니다. 볼륨과 같은 일반적인 정보와 이벤트에 대한 함수는 FSEvents 로 시작합니다. FSEventStream 로 시작하는 함수들은 새로운 이벤트 스트림을 만들고, 그 스트림을 사용하는 것 등을 담당합니다.


파일 시스템 이벤트 스트림(file system events stream) 의 생명주기는 다음과 같습니다.


  1. 스트림 생성을 하기 위해서  FSEventStreamCreate 나 FSEventStreamCreateRelativeToDevice 를 호출합니다. 최초 생성시 유지 카운트(retain count)는 1 입니다. 만약 이 카운트를 증가시키고 싶다면, FSEventStreamRetain 를 호출하면 됩니다.
  2. 스트림 생성을 하기 위해서  FSEventStreamCreate  나 FSEventStreamCreateRelativeToDevice 를 호출합니다. 최초 생성시 유지 카운트(retain count)는 1 입니다. 만약 이 카운트를 증가시키고 싶다면, FSEventStreamRetain 를 호출하면 됩니다.

  3. FSEventStreamStart 을 호출해서, 이벤트가 전송되는 것을 시작하도록 file system events 데몬에게 알립니다.

  4. 전달받은 이벤트를 처리합니다. API 는 단계 1에 정의되어있는 콜백함수의 호출을 통해서 이벤트를 전달합니다.

  5. FSEventStreamStop 를 호출해서 데몬이 더 이상 이벤트를 보내지 않도록 합니다.

  6. 만약 스트림을 다시 시작 해야 할 경우, 단계 3 을 수행합니다.

  7. FSEventStreamUnscheduleFromRunLoop 을 호출해서 실행 루프(run loop)로부터 이벤트를 스케쥴을 하지 않습니다.

  8. FSEventStreamInvalidate 를 호출해서 스트림이 유효하지 않도록 만듭니다.

  9. FSEventStreamRelease  을 호출해서 스트림의 레퍼런스를 해제 시킵니다.


다음 섹션에서 좀 더 자세히 설명하도록 하겠습니다.



Adding Include Directives


file system event stream API 를 사용하기 위해서는, 아래 Core Service framework 를 include 해야 합니다.


#include <CoreServices/CoreServices.h>




그리고 컴파일을 할 때, 반드시 Core Services Framework 를 포함시켜야 합니다.  만약 Xcode 를 사용한다면 타켓(target) 에 추가시키면 됩니다. 또는 커맨드 라인이나 Makefile 을 이용한다면, 링커 플래그(liner flag)로 -framework CoreServices 를 추가시켜야 합니다.




Creating an Event Stream


file system events API 는 두 가지 타입의 event streams 를 지원합니다. 그것은 디스크 단위(per-disk) 이벤트 스트림과 호스트 단위(per-host) 이벤트 스트림 입니다. 스트림을 만들기 전에, 디스크 단위(per-host) 이벤트 스트림과 호스트 단위(per-disk) 이벤트 스트림 중 어떤 타입으로 만들지 결정해야 합니다. 각각은  FSEventStreamCreate  와  FSEventStreamCreateRelativeToDevice 를 통해 만들 수 있습니다.


호스트 단위(per-host) 이벤트 스트림은 이벤트로 구성되어 있습니다. 이 이벤트의 ID는 호스트 안에서 다른 이벤트와 연계되어 증가합니다.이 ID 값들은 한 가지 경우를 제외하고는 고유한 값이 보장됩니다. 그 한 가지 경우는 바로 Mac OS X v10.5 이후 버젼이 설치된 다른 컴퓨터에서 사용하던 디스크를 추가할 경우입니다. 이 경우 이들 볼륨의 ID 값과 충돌이 발생 할 수 있습니다. 어쨌든 새로운 이벤트의 ID 값은 모든 디스크에서 가장 큰 ID 값 다음부터 시작합니다.


디스크 단위(per-disk) 이벤트 스트림도 역시 이벤트로 구성되어 있습니다. 그러나 대조적으로 이 이벤트의 ID는 디스크 안에서 다른 이벤트와 연계되어 증가합니다. 다른 디스크의 다른 이벤트들과 어떠한 연관도 없습니다. 그렇기 때문에 여러분이 모니터링 하고 싶은 물리적으로 다른 디스크 사이에는, 각기 별도로 이벤트 스트림을 만들어야 합니다.


보통 장기적으로 계속 유지되는 소프트웨어를 만들 경우, 디스크 단위(per-disk) 스트림을 사용해서 ID 충돌이 발생하지 않도록 합니다. 반면, 큐 디렉토리 감시와 같이 일반적인 실행을 하는 동안 디렉토리 트리나 디렉토리 변화를 감시할 것이라면, 호스트 단위(per-host) 스트림을 사용하는 것이 편리합니다.

만약 root file system 에 파일을 모니터링 한다면, 두 가지 스트림 메카니즘 모두 비슷하게 동작할 것 입니다.


아래 코드조각은 이벤트 스트림을 만드는 방법을 보여주고 있습니다.


/* 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 */
    );




이벤트 스트림을 한번 만들면, 응용프로그램의 실행 루프(run loop)에 예약해야 합니다. 그러기 위해서 새로 생성한 스트림과 실행 루프(run loop), 그리고 실행 루프 모드(run loop mode)를 가지고  FSEventStreamScheduleWithRunLoop 를 호출합니다. 실행 루프(run loop)에 대해 더 자세히 알고 싶다면, Run Loops 를 읽어보시기 바랍니다.


만약 실행 루프(run loop)를 가지고 있지 않다면, 이 태스크의 쓰레드를 사용하면 됩니다. API 를 이용해서 쓰레드를 생성한 뒤,  CFRunLoopGetCurrent 를 호출하면, 그 쓰레드의 초기 실행 루프(run loop)가 만들어 집니다. 이후에  CFRunLoopGetCurrent 를 호출하더라도 동일한 실행 루프(run loop)가 리턴됩니다.


아래 코드조각은 stream 라는 이름의 스트림을 현재 쓰레드의 실행 루프(run loop)에 예약하는 방법을 보여 줍니다.


FSEventStreamRef stream;
 /* Create the stream before calling this. */
 FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(),
         kCFRunLoopDefaultMode);




마지막으로  FSEventStreamStart 를 호출해서, 이벤트 스트림 설정을 마무리 합니다. 이 함수는 이벤트 스트림이 이벤트를 전송 시작하도록 하는 것입니다. 이 함수의 파라미터는 이벤트 스트림 한개입니다.


이벤트 스트림이 생성되고 예약하고 나도, 실행 루프(run loop)가 아직 시작되지 않았다면,  CFRunLoopRun . 를 호출해서 시작해 주어야 합니다.




Handling Events


이벤트 핸들러는 반드시  FSEventStreamCallback 프로토타입 형식에 일치해야 합니다. 파라미터는  FSEventStreamCallback  데이터 타입 문서에 나와 있는데로 되어 있습니다.


이벤트 핸들러는 경로 목록, 식별자 목록, 플래그 목록 이렇게 세 가지 목록을 전달 받습니다. 사실상 이것들은 이벤트 목록을 나타냅니다. 첫번째 이벤트에 대한 정보는 각 배열의 첫번째에 위치하고 있습니다. 나머지 이벤트도 동일한 방식입니다. 핸들러에서는 이 목록들을 순차적으로 순회하면서 필요한 이벤트를 처리해야 합니다.


각각의 이벤트에 대해, 지정된 경로의 디렉토리를 탐색하고 컨텐츠에 원하는 작업을 수행하십시오. 보통 지정된 경로의 디렉토리만 스캔하면 됩니다. 그러나 그렇지 않은 3가지 경우가 존재합니다.

  • 만약 한 디렉토리의 하위 디렉토리에서 한 개 이상의 여러 이벤트가 거의 동시에 발생할 경우, 이벤트는 하나의 이벤트에 합쳐서 단 한번만 발생합니다. 이 경우, 이벤트의  kFSEventStreamEventFlagMustScanSubDirs  플래그가 설정되어 있습니다. 이런 이벤트를 받았을 때, 반드시 이벤트에 들어있는 경로 목록을 순환적(recursively)으로 재검색 해야 합니다. 추가적인 변경사항은 경로 리스트의 바로 다음 자식(child)이 아닐 수 있습니다.

  • 만약 커널과 사용자 레벨 데몬에서 통신에러가 발생할 경우, 전달받은 이벤트는  kFSEventStreamEventFlagKernelDropped  나  kFSEventStreamEventFlagUserDropped  플래그가 설정되어 있습니다. 이 경우, 모니터링하고 있는 모든 디렉토리에 대해서 전체 스캔을 할 수 밖에 없습니다. 왜냐면 이런 상황에서는 어떤 것이 변경되었는지 알 방법이 없기 때문입니다.

  • 만약 감시하고 있는 디렉토리의 루트가 삭제되거나, 이동되거나, 이름이 변경되었을 경우(혹은 상위 디렉토리가 이동되거나 이름이 변경된 경우), 그 디렉토리는 존재하지 않게 됩니다. 이 상황을 제어하기 위해서는 스트림을 만들 때,  kFSEventStreamCreateFlagWatchRoot  플래그를 사용해야 합니다. 이렇게 하면, 상황이 발생했을 때 kFSEventStreamEventFlagRootChanged  플래그가 설정된 ID 가 0 인 이벤트를 전달 받습니다. 이 경우, 디렉토리가 존재하지 않을 수 있기 때문에, 모든 디렉토리에 대해서 다시 스캔해야만 합니다.

    만약 디렉토리가 어디로 이동되었는지 알고 싶다면,  open(1)  를 이용하여 디렉토리를 열어야 합니다. 그리고 나서  fcntl(2) 에  F_GETPATH  를 넣어 호출하면 됩니다. 좀 더 자세한 정보를 보시려면  fcntl(2)  를 참고하시기 바랍니다.

  • 이벤트 갯수가 2^64 개가 되면, 이벤트 id 값은 처음으로 돌아갑니다. 이렇게 되면, 이벤트의  kFSEventStreamEventFlagEventIdsWrapped  플래그가 설정 됩니다. 다행이도 가까운 미래에는 일어나지 않겠지만, 64 비트라는 숫자는 지구 표면(바다까지 포함)을 지우개 크기로 나눈 갯수가 약 2000 엑사바이트(2,000,000 기가바이트)인데, 이것을 충분히 저장할 만큼 큽니다. 그러나 이 플래그를 체크하고, 이 상황이 발생했을때, 적절한 조취를 취해야 합니다.

핸들러에서 현재 이벤트 스트림을 사용해 모니터링 하고 있는 경로의 리스트가 필요할 때가 있습니다. 이 경우  FSEventStreamCopyPathsBeingWatched  를 호출하면 리스트를 얻을 수 있습니다.


가끔 스트림의 어디쯤에 있는지 모니터링 하고 싶을 때가 있습니다. 예를들어, 이벤트를 처리 할건지 말건지 결정하기 위해서, 현재 이번트가 얼마나 오래된 것인지 알고 싶을 때가 있습니다.  callingFSEventStreamGetLatestEventId  를 호출하면, 현재의 이벤트 배치에서 가장 최신 이벤트를 찾아 낼 수 있습니다.  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]);
   }
}





Using Persistent Events


file system event 에서 가장 강력한 특징 중 하나는 재부팅시에도 지속된다는 점 입니다. 이것은 여러분의 응용 프로그램이 특정 시각이나 오래전에 발생한 이벤트를 쉽게 찾아낼 수 있다는 것을 의미 합니다. 그러므로 응용 프로그램이 실행중이 아닌 동안에 수정된 파일도 찾아 낼 수 있습니다. 이것은 수정된 파일들 백업, 다중 파일 프로젝트의 의존성 변경 체크 등과 같은 작업을 매우 단순하게 만들어 줍니다.


지속적 이벤트를 가지고 작업하기 위해서는, 응용 프로그램에서 작업한 마지막 이벤트의 ID 값을 저장하고 있어야 합니다. 그러면 돌아가서 어떤 파일들이 수정되었는지 확인 할 때, 마지막 작업한 이벤트 다음에 발생한 이벤트를 살펴보면 됩니다. 이후 발생한 모든 이베트를 얻기 위해서는,  FSEventStreamCreate  또는  FSEventStreamCreateRelativeToDevice 를 호출할 때,  sinceWhen  파라미터에 그 이벤트 ID 값을 전달하면 됩니다. 기기 단위(per-device) 기반인 경우, 이벤트가 발생한 시각을 얻기 위해서 타임스탬프를 쉽게 이용할 수 있습니다. 이러기 위해서는 FSEventsGetLastEventIdForDeviceBeforeTime  를 호출해서 마지막 이벤트의 ID 를 구하고,  FSEventStreamCreateRelativeToDevice 의  sinceWhen  파라미터로 넣으면 됩니다.


기기 단위(per-device) 기반에서는, 이벤트가 발생한 시각을 결정하기 위해서 타임 스탬프를 손쉽게 이용할 수 있습니다. 이렇게 하기 위해서는 먼저  FSEventsGetLastEventIdForDeviceBeforeTime  를 호출해서 기기의 마지막 이벤트의 ID 값을 구합니다. 그리고 나서 그 결과값을  FSEventStreamCreateRelativeToDevice 를 호출할 때 파라미터로 전달합니다. 자세한 내용은  “Special Considerations for Per-Device Streams.”  를 참고하시기 바랍니다.


지속적 이벤트를 가지고 작업할 때, 기본적으로 트리를 이용해서 파일의 메타 데이터의 “snapshot” 캐쉬를 사용해 파일 시스템 이벤트 알림을 하나로 합치는 기술을 필요로 합니다. 이 작업은  “Building a Directory Hierarchy Snapshot.”  를 참고하시기 바랍니다.

Building a Directory Hierarchy Snapshot


파일 시스템 이벤트는 주어진 디렉토리의 변화를 말 해 줍니다. 프린트나 메일 스풀러 응용프로그램 같은 경우에는 디렉토리에 파일이 추가 되었는지만 알면 되므로 이 정도면 충분합니다.


그러나 디렉토리에 어떤 변화가 있었는지 정확히 알아야 하는 경우라면, 이 정도로는 부족합니다. 이 문제를 해결하기 위한 가장 간단한 방법은, 그 시각에 시스템 상태를 복사해서 저장하는 방식으로 디렉토리 구조(directory hierarchy)의 스냅샷을 찍는 것입니다.  예를들어, 마지막으로 작업한 시각 이후에 어떤 파일이 수정되었는지 알기 위해서, 모든 파일의 이름과 수정 날짜를 저장하는 것입니다.


구조(hierarchy)를 순회하면서 여러분이 정의한 데이터 구조로 만들면 됩니다. 캐싱 프로세스에서 변경 내용을 발견하면, 메타 데이터를 캐시하기 위해서,  변경된 스냅샷을 얻기 위해 디렉토리를 다시 읽으면 됩니다. 당시 구조(hierarchy)의 상태를 나타내는 메타 데이터 트리를 한번 캐시하고 나면, 이후에 디렉토리 상태 스냅샷을 다시 찍어서 두 개를 비교하는 방식으로, 어떤 파일이 어떻게 변경되었는지 알아 낼 수 있습니다.

Mac OS X 에서는 이것을 쉽게 만들 수 있는 API 들을 제공합니다.  scandir(3)  은 디렉토리를 빠르게 순회할 수 있도록, 진입점 배열을 리턴해 줍니다. 이것은  opendir(3) ,  readdir(3)  등과 같은 고전적인 방식으로 디렉토리를 읽는 것보다 간편합니다. 그리고 어쨌든 캐싱하기 위해서 항상 디렉토리를 전체를 순회하는 것보다, 약간 더 효율적입니다.


커다란 검색 트리를 가지고 작업할 경우,  tsearch(3)tfind(3)twalk(3), tdelete(3)  와 같은 바이너리 트리 함수를 사용하면 더 간단합니다. 특징적으로, 이진트리는 특정 디렉토리의 캐시된 파일 정보를 빨리 찾는데 간편합니다. 다음 코드조각은 이 함수들을 사용하는 적절한 방식을 보여주고 있습니다.


Listing 2-1  Using the tsearch, tfind, twalk, and 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(3)  와 tdelete(3) 함수는 트리변수가 아닌, 트리 변수의 주소값을 필요로 합니다. 왜냐면 생성하거나 삭제할 때, 각각 트리 변수안에 값을 수정해서 저장해야 하고, 초기 루트를 삭제해야 하기 때문입니다. 

    심지어   tfind(3) 는 루트의 값을 수정하지 않고, ,자신의 루트 포인터가 아닌 첫번째 파라미터로 받은 루트의 주소값을 계속 가지고 있습니다.  가장 많이들 하는 실수가  dirtree  포인터를 넘기는 것입니다. 사실  &dirtree(dirtree 의 포인터) 를 넘겨야 합니다.

  • The values passed to the callback by twalk(3) and the values returned by tfind(3) and tsearch(3) are the address where the pointer to the data is stored, not the data value itself. Because this code passed in the address of an integer, it is necessary to dereference that value twice—once for the original address-of operator and once to dereference the pointer to that pointer that these functions return. twalk(3)  에 의해 콜백으로 전달되는 값과   tfind(3)  와  tsearch(3) 에 의해 리턴되는 값은 데이터값 자체가 아니라, 데이터가 저장되어 있는 포인터의 주소값 입니다. 코드는 정수의 주소값으로 전달되므로, 값을 두번 비교해야 할 필요가 있습니다. 한 가지는  address-of 오퍼레이터이고, 또 한가지는 이들 함수가 리턴하는 포인터의 포인터 입니다.


  • 그러나 다른 함수들과는 다르게,  tdelete(3) 는 데이터가 저장되어 주소값을 리턴하지 않습니다. 왜냐하면 트리 안에는 더 이상 데이터가 존재하지 않기 때문입니다. 대신 삭제한 노드의 부모노드를 리턴 합니다.


POSIX 함수인  stat(2)  와  lstat(2)  는 메타 데이터 파일을 엑세스 하기 쉽도록 해 줍니다. 이 두 함수는 심볼릭 링크를 다루는데 차이가 있습니다.  lstat 함수는 링크 자체에 대한 정보를 제공하는 반면,  stat  함수는 링크가 가리키는 파일에 대한 정보를 제공합니다. 일반적으로 말해서, 파일 시스템 이벤트가 동작하고 있을 때, 보통  lstat 를 사용할 것입니다. 왜냐하면, 변경에 대한 알림이 링크가 가리키고 있는 파일에 대한 알림이 아니라, 심볼릭 링크 자체의 변경에 대한 알림이기 때문입니다. 하지만 심볼릭 링크가 항상 트리의 한 부분을 가리키는 제어된 파일 구조에서 작업하는 중이라면,  stat 를 사용해야 할 것입니다.


디렉토리의 스냅샷을 찍는 툴에 대한 예제는  Watcher  샘플코드를 참고하시기 바랍니다.


Cleaning Up


시스템 이벤트 스트림이 더 이상 필요가 없을 때에는 반드시 스트림을 clean up 통해, 메모리와 디스크립터가 누수되지 않도록 해야 합니다. 그러나 clean up 을 하기 전에,  FSEventStreamStop 를 호출해서 실행 루프(run loop)를 먼저 중지시켜야 합니다.


그 다음  FSEventStreamInvalidate 를 호출해야 합니다. 이 함수는 한 번의 호출을 통해, 모든 실행 루프로부터 해당 스트림을 언스케쥴(unschedules) 시킵니다. 한 개의 실행 루프에서 언스케쥴(unschedule)시키거나, 두 개의 실행 루프 간 이벤트 스트림을 옮기는 경우라면  callFSEventStreamUnscheduleFromRunLoop 를 호출하면 됩니다. 다시 스케쥴(schedule)시키려면  FSEventStreamScheduleWithRunLoop 를 호출하면 됩니다.


이벤트 스트림을 무효화 시킨 경우에는,  FSEventStreamRelease 를 호출해서 릴리즈 시킬 수 있습니다. 유지 카운트(retain count)는 0으로 설정되면, 스트림은 해제될 것입니다.

곤란한 상황이 발생하였을 때, 유용한 클린업 관련 세 가지 함수가 있습니다. 응용 프로그램에서 특정 스트림을 클린업 하기 위해서, 파일 시스템이 안정적인 상태라는 것을 보장 받아야 할 경우가 있습니다. 이 경우 그 스트림을 비우는게(flush) 유용할 것 입니다. 이때  FSEventStreamFlushAsync  나  FSEventStreamFlushSync 를 통해서 스트림을 비울 수 있습니다.


플러싱이 시작할 때, 동기화(synchronous) 호출은 걸려있는(pending) 모든 이벤트가 비워질 때까지 리턴하지 않을 것 입니다. 비동기(asynchronous) 호출은 즉시 리턴되고, 걸려있는 가장 마지막 이벤트의 이벤트 ID( FSEventStreamEventId 타입)값을 리턴할 것 입니다. 이 값은 마지막 이벤트가 처리 되었는지 확인하기 위해서, 콜벡 함수 안에서 사용 할 수 있습니다.


클린업과 관련된 마지막 함수는  FSEventsPurgeEventsForDeviceUpToEventId 입니다. 이 함수는 슈퍼 유저(root user)만 호출 할 수 있습니다. 왜냐면 이 함수는 특정 이벤트 ID 이전의 모든 이벤트 기록을 삭제하기 때문입니다. 이벤트 데이터를 사용하는 응용 프로그램이 여러분 응용 프로그램만 있는 것이 아니므로, 이 함수를 호출하지 않는 것이 일반적인 룰 입니다.


만약 전문적인 응용 프로그램(예를 들어, 엔터프라이즈 백업 솔루션) 을 개발하고 있다면, 이 함수를 호출 함으로써 이벤트 기록이 계속해서 누적되지 않고, 적합한 크기로 기록 되도록 할 수 있습니다. 그러나 이것은 관리자가 명시적으로 이 행동을 요청했을 때만 수행해야 합니다. 또한 항상 사용자에게 수행전 확인을 해야 합니다.(이전에 수행한 적이 있든, 이후에 이런 동작을 수행할 수 있다는 내용을 언급 했더라도)


Special Considerations for Per-Device Streams

“Handling Events”  에는  FSEventStreamCreateRelativeToDevice 를 통해 생성한 스트림인 기기 단위(per-device) 스트림에 대해 고려해야 할 몇가지 특징이 나와 있습니다. 

  • 모든 경로(path)는 모니터링 하는 볼륨의 루트에 대한 상대경로입니다. 시스템 루트에 대한 상대경로가 아닙니다. 이것은 스트림을 생성하거나 이벤트 콜백에서 전달받는 경로 모두에 해당합니다.

  • 디바이스 ID 는 재부팅(특히, 이동식 디바이스)시에 값이 변할 수 있습니다. 감시중인 볼륨이 올바른지 UUID 값을 비교해서 확인해야 하는 책임은 여러분에게 있습니다.

스트림에서 사용중인 디바이스의 UUID 값은 모두  FSEventStreamGetDeviceBeingWatched 를 통해서 구할 수 있습니다.


디바이스의 고유한 ID 값은  FSEventsCopyUUIDForDevice 를 통해 얻을 수 있습니다. 이 고유한 ID 값이 이전에 구한 값과 다르다면, 이유는 여러가지가 될 수 있습니다. 여러분이 변경한 볼륨의 이름이 이미 존재해서, 동일한 이름의 볼륨이 두 개가 되어버린 경우거나, 해당 볼륨의 이벤트 ID 들이 초기화된 경우 입니다. 또는 볼륨의 이전 이벤트들이 다른 볼륨에서는 유효하더라도, 이 볼륨에 대해서 유효하지 않은 경우입니다.


만약 볼륨에서 이전에 저장해 두었던 UUID 를 찾더라도, 이벤트 ID 가 가장 마지막에 저장한 값보다 작다면, 사용자가 백업해 볼륨으로 복구했거나 ID 값이 한바퀴를 돌아 처음으로 돌아왔음을 의미합니다. 또는 초기화 된 것을 의미합니다. 이들 경우 모두, 저장된 이벤트들은 모두 더 이상 유효하지 않습니다.


마지막으로, 지속적 이벤트(persistent events)를 사용할 경우, 타임 스탬프 이전의 마지막 이벤트를 얻기 위해서  FSEventsGetLastEventIdForDeviceBeforeTime  함수를 사용 할 수 있습니다. 이 이벤트 ID 는 연속적이기 때문에, 증분에 대한 백업을 수행하는데 매우 유용할 수 있습니다.


시각 형식은  CFAbsoluteTime  값을 사용합니다. 이것은 2001 년 1월 1일로부터 지나간 시간을 초 단위로 나타냅니다. 다른 타임스탬프 형식을 사용하려면 아래 방식을 통해서 이 형식으로 변환시켜야 합니다.

  • Cocoa 응용 프로그램을 만들고 있다면, 변환을 위해  NSDate  객체를 사용해야 합니다.  CFAbsoluteTime  값을 얻기 위해서는  CFDateGetAbsoluteTime  를 사용해야 합니다.( NSDate  객체는  CFDateRef 로 변환할 수 있습니다.)

  • 만약 Cocoa 응용 프로그램이 아닌 곳에서 POSIX 타임 스탬프를 사용하고 있다면, 그 값에서  kCFAbsoluteTimeIntervalSince1970  를 빼면,  CFAbsoluteTime  를 구할 수 있습니다. 단, 타임스탬프가 GMT 기반이어야 합니다.

  • 만약 Cocoa 응용 프로그램이 아닌 곳에서 Carbon 타임 스탬프를 사용하고 있다면,   kCFAbsoluteTimeIntervalSince1904 를 빼면 됩니다. 단, 타임스탬프가  GMT 기반이어야 합니다.


날짜와 시간 타입에 관련된 더 많은 정보는 Date and Time Programming Guide for Core Foundation  를 참고하시기 바랍니다.



원문 : https://developer.apple.com/library/mac/#documentation/Darwin/Conceptual/FSEvents_ProgGuide/UsingtheFSEventsFramework/UsingtheFSEventsFramework.html#//apple_ref/doc/uid/TP40005289-CH4-SW4