/* fswatcher.m * * Copyright (C) 2004 Free Software Foundation, Inc. * * Author: Enrico Sersale * Date: February 2004 * * This file is part of the GNUstep GWorkspace application * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111 USA. */ #include "fswatcher.h" #include "config.h" #define GWDebugLog(format, args...) \ do { if (GW_DEBUG_LOG) \ NSLog(format , ## args); } while (0) @implementation FSWClientInfo - (void)dealloc { TEST_RELEASE (conn); TEST_RELEASE (client); RELEASE (wpaths); [super dealloc]; } - (id)init { self = [super init]; if (self) { client = nil; conn = nil; wpaths = [[NSCountedSet alloc] initWithCapacity: 1]; global = NO; } return self; } - (void)setConnection:(NSConnection *)connection { ASSIGN (conn, connection); } - (NSConnection *)connection { return conn; } - (void)setClient:(id )clnt { ASSIGN (client, clnt); } - (id )client { return client; } - (void)addWatchedPath:(NSString *)path { [wpaths addObject: path]; } - (void)removeWatchedPath:(NSString *)path { [wpaths removeObject: path]; } - (BOOL)isWathchingPath:(NSString *)path { return [wpaths containsObject: path]; } - (NSSet *)watchedPaths { return wpaths; } - (void)setGlobal:(BOOL)value { global = value; } - (BOOL)isGlobal { return global; } @end @implementation FSWatcher - (void)dealloc { int i; for (i = 0; i < [clientsInfo count]; i++) { NSConnection *connection = [[clientsInfo objectAtIndex: i] connection]; if (connection) { [nc removeObserver: self name: NSConnectionDidDieNotification object: connection]; } } if (conn) { [nc removeObserver: self name: NSConnectionDidDieNotification object: conn]; DESTROY (conn); } [dnc removeObserver: self]; RELEASE (clientsInfo); NSZoneFree (NSDefaultMallocZone(), (void *)watchers); freeTree(includePathsTree); freeTree(excludePathsTree); RELEASE (excludedSuffixes); [super dealloc]; } - (id)init { self = [super init]; if (self) { fm = [NSFileManager defaultManager]; nc = [NSNotificationCenter defaultCenter]; dnc = [NSDistributedNotificationCenter defaultCenter]; conn = [NSConnection defaultConnection]; [conn setRootObject: self]; [conn setDelegate: self]; if ([conn registerName: @"fswatcher"] == NO) { NSLog(@"unable to register with name server - quiting."); DESTROY (self); return self; } clientsInfo = [NSMutableArray new]; watchers = NSCreateMapTable(NSObjectMapKeyCallBacks, NSObjectMapValueCallBacks, 0); includePathsTree = newTreeWithIdentifier(@"incl_paths"); excludePathsTree = newTreeWithIdentifier(@"excl_paths"); excludedSuffixes = [[NSMutableSet alloc] initWithCapacity: 1]; [self setDefaultGlobalPaths]; [nc addObserver: self selector: @selector(connectionBecameInvalid:) name: NSConnectionDidDieNotification object: conn]; [dnc addObserver: self selector: @selector(globalPathsChanged:) name: @"GSMetadataIndexedDirectoriesChanged" object: nil]; } return self; } - (BOOL)connection:(NSConnection *)ancestor shouldMakeNewConnection:(NSConnection *)newConn; { FSWClientInfo *info = [FSWClientInfo new]; [info setConnection: newConn]; [clientsInfo addObject: info]; RELEASE (info); [nc addObserver: self selector: @selector(connectionBecameInvalid:) name: NSConnectionDidDieNotification object: newConn]; [newConn setDelegate: self]; return YES; } - (void)connectionBecameInvalid:(NSNotification *)notification { id connection = [notification object]; [nc removeObserver: self name: NSConnectionDidDieNotification object: connection]; if (connection == conn) { NSLog(@"argh - fswatcher server root connection has been destroyed."); exit(EXIT_FAILURE); } else { FSWClientInfo *info = [self clientInfoWithConnection: connection]; if (info) { NSSet *wpaths = [info watchedPaths]; NSEnumerator *enumerator = [wpaths objectEnumerator]; NSString *wpath; while ((wpath = [enumerator nextObject])) { Watcher *watcher = [self watcherForPath: wpath]; if (watcher) { [watcher removeListener]; } } [clientsInfo removeObject: info]; } } } - (void)setDefaultGlobalPaths { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; id entry; unsigned i; [defaults synchronize]; entry = [defaults arrayForKey: @"GSMetadataIndexablePaths"]; if (entry) { for (i = 0; i < [entry count]; i++) { insertComponentsOfPath([entry objectAtIndex: i], includePathsTree); } } else { insertComponentsOfPath(NSHomeDirectory(), includePathsTree); entry = NSSearchPathForDirectoriesInDomains(NSAllApplicationsDirectory, NSAllDomainsMask, YES); for (i = 0; i < [entry count]; i++) { insertComponentsOfPath([entry objectAtIndex: i], includePathsTree); } entry = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, YES); for (i = 0; i < [entry count]; i++) { NSString *dir = [entry objectAtIndex: i]; NSString *path = [dir stringByAppendingPathComponent: @"Headers"]; if ([fm fileExistsAtPath: path]) { insertComponentsOfPath(path, includePathsTree); } path = [dir stringByAppendingPathComponent: @"Documentation"]; if ([fm fileExistsAtPath: path]) { insertComponentsOfPath(path, includePathsTree); } } } entry = [defaults arrayForKey: @"GSMetadataExcludedPaths"]; if (entry) { for (i = 0; i < [entry count]; i++) { insertComponentsOfPath([entry objectAtIndex: i], excludePathsTree); } } entry = [defaults arrayForKey: @"GSMetadataExcludedSuffixes"]; if (entry == nil) { entry = [NSArray arrayWithObjects: @"a", @"d", @"dylib", @"er1", @"err", @"extinfo", @"frag", @"la", @"log", @"o", @"out", @"part", @"sed", @"so", @"status", @"temp", @"tmp", nil]; } [excludedSuffixes addObjectsFromArray: entry]; } - (void)globalPathsChanged:(NSNotification *)notification { NSDictionary *info = [notification userInfo]; NSArray *indexable = [info objectForKey: @"GSMetadataIndexablePaths"]; NSArray *excluded = [info objectForKey: @"GSMetadataExcludedPaths"]; NSArray *suffixes = [info objectForKey: @"GSMetadataExcludedSuffixes"]; unsigned i; emptyTreeWithBase(includePathsTree); for (i = 0; i < [indexable count]; i++) { insertComponentsOfPath([indexable objectAtIndex: i], includePathsTree); } emptyTreeWithBase(excludePathsTree); for (i = 0; i < [excluded count]; i++) { insertComponentsOfPath([excluded objectAtIndex: i], excludePathsTree); } [excludedSuffixes removeAllObjects]; [excludedSuffixes addObjectsFromArray: suffixes]; } - (oneway void)registerClient:(id )client isGlobalWatcher:(BOOL)global { NSConnection *connection = [(NSDistantObject *)client connectionForProxy]; FSWClientInfo *info = [self clientInfoWithConnection: connection]; if (info == nil) { [NSException raise: NSInternalInconsistencyException format: @"registration with unknown connection"]; } if ([info client] != nil) { [NSException raise: NSInternalInconsistencyException format: @"registration with registered client"]; } if ([(id)client isProxy] == YES) { [(id)client setProtocolForProxy: @protocol(FSWClientProtocol)]; [info setClient: client]; [info setGlobal: global]; } } - (oneway void)unregisterClient:(id )client { NSConnection *connection = [(NSDistantObject *)client connectionForProxy]; FSWClientInfo *info = [self clientInfoWithConnection: connection]; NSSet *wpaths; NSEnumerator *enumerator; NSString *wpath; if (info == nil) { [NSException raise: NSInternalInconsistencyException format: @"unregistration with unknown connection"]; } if ([info client] == nil) { [NSException raise: NSInternalInconsistencyException format: @"unregistration with unregistered client"]; } wpaths = [info watchedPaths]; enumerator = [wpaths objectEnumerator]; while ((wpath = [enumerator nextObject])) { Watcher *watcher = [self watcherForPath: wpath]; if (watcher) { [watcher removeListener]; } } [nc removeObserver: self name: NSConnectionDidDieNotification object: connection]; [clientsInfo removeObject: info]; } - (FSWClientInfo *)clientInfoWithConnection:(NSConnection *)connection { int i; for (i = 0; i < [clientsInfo count]; i++) { FSWClientInfo *info = [clientsInfo objectAtIndex: i]; if ([info connection] == connection) { return info; } } return nil; } - (FSWClientInfo *)clientInfoWithRemote:(id)remote { int i; for (i = 0; i < [clientsInfo count]; i++) { FSWClientInfo *info = [clientsInfo objectAtIndex: i]; if ([info client] == remote) { return info; } } return nil; } - (oneway void)client:(id )client addWatcherForPath:(NSString *)path { NSConnection *connection = [(NSDistantObject *)client connectionForProxy]; FSWClientInfo *info = [self clientInfoWithConnection: connection]; Watcher *watcher = [self watcherForPath: path]; if (info == nil) { [NSException raise: NSInternalInconsistencyException format: @"adding watcher from unknown connection"]; } if ([info client] == nil) { [NSException raise: NSInternalInconsistencyException format: @"adding watcher for unregistered client"]; } if (watcher) { GWDebugLog(@"watcher found; adding listener for: %@", path); [info addWatchedPath: path]; [watcher addListener]; } else { if ([fm fileExistsAtPath: path]) { GWDebugLog(@"add watcher for: %@", path); [info addWatchedPath: path]; watcher = [[Watcher alloc] initWithWatchedPath: path fswatcher: self]; NSMapInsert (watchers, path, watcher); RELEASE (watcher); } } } - (oneway void)client:(id )client removeWatcherForPath:(NSString *)path { NSConnection *connection = [(NSDistantObject *)client connectionForProxy]; FSWClientInfo *info = [self clientInfoWithConnection: connection]; Watcher *watcher = [self watcherForPath: path]; if (info == nil) { [NSException raise: NSInternalInconsistencyException format: @"removing watcher from unknown connection"]; } if ([info client] == nil) { [NSException raise: NSInternalInconsistencyException format: @"removing watcher for unregistered client"]; } if (watcher && ([watcher isOld] == NO)) { GWDebugLog(@"remove listener for: %@", path); [info removeWatchedPath: path]; [watcher removeListener]; } } - (Watcher *)watcherForPath:(NSString *)path { return (Watcher *)NSMapGet(watchers, path); } - (void)watcherTimeOut:(NSTimer *)sender { Watcher *watcher = (Watcher *)[sender userInfo]; if ([watcher isOld]) { [self removeWatcher: watcher]; } else { [watcher watchFile]; } } - (void)removeWatcher:(Watcher *)watcher { NSString *path = [watcher watchedPath]; NSTimer *timer = [watcher timer]; if (timer && [timer isValid]) { [timer invalidate]; } GWDebugLog(@"removed watcher for: %@", path); RETAIN (path); NSMapRemove(watchers, path); RELEASE (path); } - (pcomp *)includePathsTree { return includePathsTree; } - (pcomp *)excludePathsTree { return excludePathsTree; } - (NSSet *)excludedSuffixes { return excludedSuffixes; } static inline BOOL isDotFile(NSString *path) { int len = ([path length] - 1); static unichar sep = 0; unichar c; int i; if (sep == 0) { #if defined(__MINGW32__) sep = '\\'; #else sep = '/'; #endif } for (i = len; i >= 0; i--) { c = [path characterAtIndex: i]; if (c == '.') { if ((i > 0) && ([path characterAtIndex: (i - 1)] == sep)) { return YES; } } } return NO; } - (BOOL)isGlobalValidPath:(NSString *)path { NSString *ext = [[path pathExtension] lowercaseString]; return (([excludedSuffixes containsObject: ext] == NO) && (isDotFile(path) == NO) && inTreeFirstPartOfPath(path, includePathsTree) && (inTreeFirstPartOfPath(path, excludePathsTree) == NO)); } - (void)notifyClients:(NSDictionary *)info { CREATE_AUTORELEASE_POOL(pool); NSString *path = [info objectForKey: @"path"]; NSString *event = [info objectForKey: @"event"]; NSData *data = [NSArchiver archivedDataWithRootObject: info]; int i; for (i = 0; i < [clientsInfo count]; i++) { FSWClientInfo *clinfo = [clientsInfo objectAtIndex: i]; if ([clinfo isWathchingPath: path]) { [[clinfo client] watchedPathDidChange: data]; } } if ([event isEqual: @"GWWatchedPathDeleted"] && [self isGlobalValidPath: path]) { GWDebugLog(@"DELETE %@", path); [self notifyGlobalWatchingClients: info]; } else if ([event isEqual: @"GWWatchedFileModified"] && [self isGlobalValidPath: path]) { GWDebugLog(@"MODIFIED %@", path); [self notifyGlobalWatchingClients: info]; } else if ([event isEqual: @"GWFileDeletedInWatchedDirectory"]) { NSArray *files = [info objectForKey: @"files"]; for (i = 0; i < [files count]; i++) { NSString *fname = [files objectAtIndex: i]; NSString *fullpath = [path stringByAppendingPathComponent: fname]; if ([self isGlobalValidPath: fullpath]) { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; [dict setObject: fullpath forKey: @"path"]; [dict setObject: @"GWWatchedPathDeleted" forKey: @"event"]; [self notifyGlobalWatchingClients: dict]; } } } else if ([event isEqual: @"GWFileCreatedInWatchedDirectory"]) { NSArray *files = [info objectForKey: @"files"]; for (i = 0; i < [files count]; i++) { NSString *fname = [files objectAtIndex: i]; NSString *fullpath = [path stringByAppendingPathComponent: fname]; if ([self isGlobalValidPath: fullpath]) { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; [dict setObject: fullpath forKey: @"path"]; [dict setObject: @"GWFileCreatedInWatchedDirectory" forKey: @"event"]; [self notifyGlobalWatchingClients: dict]; } } } RELEASE (pool); } - (void)notifyGlobalWatchingClients:(NSDictionary *)info { int i; for (i = 0; i < [clientsInfo count]; i++) { FSWClientInfo *clinfo = [clientsInfo objectAtIndex: i]; if ([clinfo isGlobal]) { [[clinfo client] globalWatchedPathDidChange: info]; } } } @end @implementation Watcher - (void)dealloc { if (timer && [timer isValid]) { [timer invalidate]; } RELEASE (watchedPath); TEST_RELEASE (pathContents); RELEASE (date); [super dealloc]; } - (id)initWithWatchedPath:(NSString *)path fswatcher:(id)fsw { self = [super init]; if (self) { NSDictionary *attributes; NSString *type; ASSIGN (watchedPath, path); fm = [NSFileManager defaultManager]; attributes = [fm fileAttributesAtPath: path traverseLink: YES]; type = [attributes fileType]; ASSIGN (date, [attributes fileModificationDate]); if (type == NSFileTypeDirectory) { ASSIGN (pathContents, ([fm directoryContentsAtPath: watchedPath])); isdir = YES; } else { isdir = NO; } fswatcher = fsw; listeners = 1; isOld = NO; timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: fswatcher selector: @selector(watcherTimeOut:) userInfo: self repeats: YES]; } return self; } - (void)watchFile { CREATE_AUTORELEASE_POOL(pool); NSDictionary *attributes; NSDate *moddate; NSMutableDictionary *notifdict; if (isOld) { RELEASE (pool); return; } attributes = [fm fileAttributesAtPath: watchedPath traverseLink: YES]; if (attributes == nil) { notifdict = [NSMutableDictionary dictionary]; [notifdict setObject: watchedPath forKey: @"path"]; [notifdict setObject: @"GWWatchedPathDeleted" forKey: @"event"]; [fswatcher notifyClients: notifdict]; isOld = YES; RELEASE (pool); return; } moddate = [attributes fileModificationDate]; if ([date isEqualToDate: moddate] == NO) { if (isdir) { NSArray *oldconts = [pathContents copy]; NSArray *newconts = [fm directoryContentsAtPath: watchedPath]; NSMutableArray *diffFiles = [NSMutableArray array]; BOOL contentsChanged = NO; int i; ASSIGN (date, moddate); ASSIGN (pathContents, newconts); notifdict = [NSMutableDictionary dictionary]; [notifdict setObject: watchedPath forKey: @"path"]; /* if there is an error in fileAttributesAtPath */ /* or watchedPath doesn't exist anymore */ if (newconts == nil) { [notifdict setObject: @"GWWatchedPathDeleted" forKey: @"event"]; [fswatcher notifyClients: notifdict]; RELEASE (oldconts); isOld = YES; RELEASE (pool); return; } for (i = 0; i < [oldconts count]; i++) { NSString *fname = [oldconts objectAtIndex: i]; if ([newconts containsObject: fname] == NO) { [diffFiles addObject: fname]; } } if ([diffFiles count] > 0) { contentsChanged = YES; [notifdict setObject: @"GWFileDeletedInWatchedDirectory" forKey: @"event"]; [notifdict setObject: diffFiles forKey: @"files"]; [fswatcher notifyClients: notifdict]; } [diffFiles removeAllObjects]; for (i = 0; i < [newconts count]; i++) { NSString *fname = [newconts objectAtIndex: i]; if ([oldconts containsObject: fname] == NO) { [diffFiles addObject: fname]; } } if ([diffFiles count] > 0) { contentsChanged = YES; [notifdict setObject: watchedPath forKey: @"path"]; [notifdict setObject: @"GWFileCreatedInWatchedDirectory" forKey: @"event"]; [notifdict setObject: diffFiles forKey: @"files"]; [fswatcher notifyClients: notifdict]; } TEST_RELEASE (oldconts); if (contentsChanged == NO) { [notifdict setObject: @"GWWatchedFileModified" forKey: @"event"]; [fswatcher notifyClients: notifdict]; } } else { // isdir == NO ASSIGN (date, moddate); notifdict = [NSMutableDictionary dictionary]; [notifdict setObject: watchedPath forKey: @"path"]; [notifdict setObject: @"GWWatchedFileModified" forKey: @"event"]; [fswatcher notifyClients: notifdict]; } } RELEASE (pool); } - (void)addListener { listeners++; } - (void)removeListener { listeners--; if (listeners <= 0) { isOld = YES; } } - (BOOL)isWathcingPath:(NSString *)apath { return ([apath isEqualToString: watchedPath]); } - (NSString *)watchedPath { return watchedPath; } - (BOOL)isOld { return isOld; } - (NSTimer *)timer { return timer; } @end int main(int argc, char** argv) { CREATE_AUTORELEASE_POOL(pool); NSProcessInfo *info = [NSProcessInfo processInfo]; NSMutableArray *args = AUTORELEASE ([[info arguments] mutableCopy]); static BOOL is_daemon = NO; BOOL subtask = YES; if ([[info arguments] containsObject: @"--daemon"]) { subtask = NO; is_daemon = YES; } if (subtask) { NSTask *task = [NSTask new]; NS_DURING { [args removeObjectAtIndex: 0]; [args addObject: @"--daemon"]; [task setLaunchPath: [[NSBundle mainBundle] executablePath]]; [task setArguments: args]; [task setEnvironment: [info environment]]; [task launch]; DESTROY (task); } NS_HANDLER { fprintf (stderr, "unable to launch the fswatcher task. exiting.\n"); DESTROY (task); } NS_ENDHANDLER exit(EXIT_FAILURE); } RELEASE(pool); { CREATE_AUTORELEASE_POOL (pool); FSWatcher *fsw = [[FSWatcher alloc] init]; RELEASE (pool); if (fsw != nil) { CREATE_AUTORELEASE_POOL (pool); [[NSRunLoop currentRunLoop] run]; RELEASE (pool); } } exit(EXIT_SUCCESS); }