1from FSEvents import *
2import objc
3import sys
4import os
5import stat
6import errno
7
8def T_or_F(x):
9    if x:
10        return "TRUE"
11    else:
12        return "FALSE"
13
14class Settings (object):
15    __slots__ = (
16            'sinceWhen',
17            'latency',
18            'flags',
19            'array_of_paths',
20            'print_settings',
21            'verbose',
22            'flush_seconds',
23    )
24    def __init__(self):
25        self.sinceWhen = kFSEventStreamEventIdSinceNow
26        self.latency   = 60
27        self.flags     = 0
28        self.array_of_paths = []
29        self.print_settings = False
30        self.verbose = False
31        self.flush_seconds = -1
32
33    def mesg(self, fmt, *args, **kwds):
34        if args:
35            fmt = fmt % args
36
37        elif kwds:
38            fmt = fmt % kwds
39
40        if self.verbose:
41            print >>sys.stderr, fmt
42        else:
43            print >>sys.stdout, fmt
44
45    def debug(self, fmt, *args, **kwds):
46        if not self.verbose:
47            return
48
49        if args:
50            fmt = fmt % args
51
52        elif kwds:
53            fmt = fmt % kwds
54
55        print >>sys.stderr, fmt
56
57    def error(self, fmt, *args, **kwds):
58        if args:
59            fmt = fmt % args
60
61        elif kwds:
62            fmt = fmt % kwds
63
64        print >>sys.stderr, fmt
65
66    def dump(self):
67        self.mesg("settings->sinceWhen = %d", self.sinceWhen)
68        self.mesg("settings->latency = %f", self.latency)
69        self.mesg("settings->flags = %#x", self.flags)
70        self.mesg("settings->num_paths = %d", len(self.array_of_paths))
71        for idx, path in enumerate(self.array_of_paths):
72            self.mesg("settings->array_of_paths[%d] = '%s'", idx, path)
73        self.mesg("settings->verbose = %s", T_or_F(self.verbose))
74        self.mesg("settings->print_settings = %s", T_or_F(self.print_settings))
75        self.mesg("settings->flush_seconds = %d", self.flush_seconds)
76
77    def parse_argv(self, argv):
78        self.latency = 1.0
79        self.sinceWhen = -1 # kFSEventStreamEventIdSinceNow
80        self.flush_seconds = -1
81
82        idx = 1
83        while idx < len(argv):
84            if argv[idx] == '-usage':
85                usage(argv[0])
86
87            elif argv[idx] == '-print_settings':
88                self.print_settings = True
89
90            elif argv[idx] == '-sinceWhen':
91                self.sinceWhen = int(argv[idx+1])
92                idx += 1
93
94            elif argv[idx] == '-latency':
95                self.latency = float(argv[idx+1])
96                idx += 1
97
98            elif argv[idx] == '-flags':
99                self.flags = int(argv[idx+1])
100                idx += 1
101
102            elif argv[idx] == '-flush':
103                self.flush_seconds = float(argv[idx+1])
104                idx += 1
105
106            elif argv[idx] == '-verbose':
107                self.verbose = True
108
109            else:
110                break
111
112            idx += 1
113
114        self.array_of_paths = argv[idx:]
115
116settings = Settings()
117
118def usage(progname):
119    settings.mesg("")
120    settings.mesg("Usage: %s <flags> <path>", progname)
121    settings.mesg("Flags:")
122    settings.mesg("       -sinceWhen <when>          Specify a time from whence to search for applicable events")
123    settings.mesg("       -latency <seconds>         Specify latency")
124    settings.mesg("       -flags <flags>             Specify flags as a number")
125    settings.mesg("       -flush <seconds>           Invoke FSEventStreamFlushAsync() after the specified number of seconds.")
126    settings.mesg("")
127    sys.exit(1)
128
129
130def timer_callback(timer, streamRef):
131    settings.debug("CFAbsoluteTimeGetCurrent() => %.3f", CFAbsoluteTimeGetCurrent())
132    settings.debug("FSEventStreamFlushAsync(streamRef = %s)", streamRef)
133    FSEventStreamFlushAsync(streamRef)
134
135def fsevents_callback(streamRef, clientInfo, numEvents, eventPaths, eventMasks, eventIDs):
136    settings.debug("fsevents_callback(streamRef = %s, clientInfo = %s, numEvents = %s)", streamRef, clientInfo, numEvents)
137    settings.debug("fsevents_callback: FSEventStreamGetLatestEventId(streamRef) => %s", FSEventStreamGetLatestEventId(streamRef))
138    full_path = clientInfo
139
140    for i in range(numEvents):
141        path = eventPaths[i]
142        if path[-1] == '/':
143            path = path[:-1]
144
145        if eventMasks[i] & kFSEventStreamEventFlagMustScanSubDirs:
146            recursive = True
147
148        elif eventMasks[i] & kFSEventStreamEventFlagUserDropped:
149            settings.mesg("BAD NEWS! We dropped events.")
150            settings.mesg("Forcing a full rescan.")
151            recursive = 1
152            path = full_path
153
154        elif eventMasks[i] & kFSEventStreamEventFlagKernelDropped:
155            settings.mesg("REALLY BAD NEWS! The kernel dropped events.")
156            settings.mesg("Forcing a full rescan.")
157            recursive = 1
158            path = full_path
159
160        else:
161            recursive = False
162
163        new_size = get_directory_size(path, recursive)
164        if new_size < 0:
165            print "Could not update size on %s"%(path,)
166
167        else:
168            print "New total size: %d (change made to %s) for path: %s"%(
169                    get_total_size(), path, full_path)
170
171
172def my_FSEventStreamCreate(path):
173    if settings.verbose:
174        print [path]
175
176    streamRef = FSEventStreamCreate(kCFAllocatorDefault,
177                                    fsevents_callback,
178                                    path,
179                                    [path],
180                                    settings.sinceWhen,
181                                    settings.latency,
182                                    settings.flags)
183    if streamRef is None:
184        settings.error("ERROR: FSEVentStreamCreate() => NULL")
185        return None
186
187    if settings.verbose:
188        FSEventStreamShow(streamRef)
189
190    return streamRef
191
192def main(argv=None):
193    if argv is None:
194        argv = sys.argv
195
196    settings.parse_argv(argv)
197
198    if settings.verbose or settings.print_settings:
199        settings.dump()
200
201    if settings.print_settings:
202        return 0
203
204    if len(settings.array_of_paths) != 1:
205        usage(argv[0])
206
207    full_path = os.path.abspath(settings.array_of_paths[0])
208
209    streamRef = my_FSEventStreamCreate(full_path)
210
211    FSEventStreamScheduleWithRunLoop(streamRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode)
212
213    startedOK = FSEventStreamStart(streamRef)
214    if not startedOK:
215        settings.error("failed to start the FSEventStream")
216        return
217
218    # NOTE: we get the initial size *after* we start the
219    #       FSEventStream so that there is no window
220    #       during which we would miss events.
221    #
222    dir_sz = get_directory_size(full_path, 1)
223    print "Initial total size is: %d for path: %s"%(get_total_size(), full_path)
224
225    if settings.flush_seconds >= 0:
226        settings.debug("CFAbsoluteTimeGetCurrent() => %.3f", CFAbsoluteTimeGetCurrent())
227
228        timer = CFRunLoopTimerCreate(
229                FSEventStreamGetSinceWhen(streamRef),
230                CFAbsoluteTimeGetCurrent() + settings.flush_seconds,
231                settings.flush_seconds,
232                0, 0, timer_callback, streamRef)
233        CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode)
234
235
236    # Run
237    CFRunLoopRun()
238
239    #Stop / Invalidate / Release
240    FSEventStreamStop(streamRef)
241    FSEventStreamInvalidate(streamRef)
242    #FSEventStreamRelease(streamRef)
243    return
244
245
246#
247#--------------------------------------------------------------------------------
248# Routines to keep track of the size of the directory hierarchy
249# we are watching.
250#
251# This code is not exemplary in any way.  It should definitely
252# not be used in production code as it is inefficient.
253#
254
255class dir_item (object):
256    __slots__ = ('dirname', 'size')
257
258dir_items = {}
259
260def get_total_size():
261    return sum(dir_items.itervalues())
262
263def iterate_subdirs(dirname, recursive):
264    dir_items[dirname] = 0
265
266    try:
267        names = os.listdir(dirname)
268    except os.error, msg:
269        print msg.errno, errno.EPERM
270        if msg.errno in (errno.ENOENT, errno.EPERM, errno.EACCES):
271            del dir_items[dirname]
272            return 0
273
274        raise
275
276    size = 0
277    for nm in names:
278        full_path = os.path.join(dirname, nm)
279        st = os.lstat(full_path)
280        size += st.st_size
281
282        if stat.S_ISDIR(st.st_mode) and (recursive or (full_path not in dir_items)):
283            result = get_directory_size(full_path, 1)
284
285    dir_items[dirname] = size
286    return size
287
288
289def check_for_deleted_dirs():
290    for path in dir_items.keys():
291        try:
292            os.stat(path)
293        except os.error:
294            del dir_items[path]
295
296def get_directory_size(dirname, recursive):
297    check_for_deleted_dirs()
298    return iterate_subdirs(dirname, recursive)
299
300if __name__ == "__main__":
301    main()
302