Added a prompt message for when stdin and stdout are a tty (i.e., when the user simply types “growlnotify foo” and doesn't specify a description).
2 Copyright (c) The Growl Project, 2004-2005
6 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
9 1. Redistributions of source code must retain the above copyright
10 notice, this list of conditions and the following disclaimer.
11 2. Redistributions in binary form must reproduce the above copyright
12 notice, this list of conditions and the following disclaimer in the
13 documentation and/or other materials provided with the distribution.
14 3. Neither the name of Growl nor the names of its contributors
15 may be used to endorse or promote products derived from this software
16 without specific prior written permission.
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22 #import <Foundation/Foundation.h>
23 #import "GrowlDefines.h"
24 #import "GrowlDefinesInternal.h"
25 #import "GrowlPathway.h"
26 #import "MD5Authenticator.h"
27 #include "GrowlUDPUtils.h"
29 #include "CFGrowlAdditions.h"
33 #include <netinet/in.h>
34 #include <sys/types.h>
35 #include <sys/socket.h>
38 #define NOTIFICATION_NAME CFSTR("Command-Line Growl Notification")
40 #define STRINGIFY(x) STRINGIFY2(x)
41 #define STRINGIFY2(x) #x
43 static const char usage[] =
44 "Usage: growlnotify [-hsvuwc] [-i ext] [-I filepath] [--image filepath]\n"
45 " [-a appname] [-p priority] [-H host] [-P password]\n"
46 " [--port port] [-n name] [-A method] [--progress value]\n"
47 " [--html] [-m message] [-t] [title]\n"
49 " -h,--help Display this help\n"
50 " -v,--version Display version number\n"
51 " -n,--name Set the name of the application that sends the notification\n"
52 " [Default: growlnotify]\n"
53 " -s,--sticky Make the notification sticky\n"
54 " -a,--appIcon Specify an application name to take the icon from\n"
55 " -i,--icon Specify a file type or extension to look up for the\n"
56 " notification icon\n"
57 " -I,--iconpath Specify a file whose icon will be the notification icon\n"
58 " --image Specify an image file to be used for the notification icon\n"
59 " -m,--message Sets the message to be used instead of using stdin\n"
60 " Passing - as the argument means read from stdin\n"
61 " -p,--priority Specify an int or named key (default is 0)\n"
62 " -d,--identifier Specify a notification identifier (used for coalescing)\n"
63 " -H,--host Specify a hostname to which to send a remote notification.\n"
64 " -P,--password Password used for remote notifications.\n"
65 " -u,--udp Use UDP instead of DO to send a remote notification.\n"
66 " --port Port number for UDP notifications.\n"
67 " -A,--auth Specify digest algorithm for UDP authentication.\n"
68 " Either MD5 [Default], SHA256 or NONE.\n"
69 " -c,--crypt Encrypt UDP notifications.\n"
70 " -w,--wait Wait until the notification has been dismissed.\n"
71 " --progress Set a progress value for this notification.\n"
73 "Display a notification using the title given on the command-line and the\n"
74 "message given in the standard input.\n"
76 "Priority can be one of the following named keys: Very Low, Moderate, Normal,\n"
77 "High, Emergency. It can also be an int between -2 and 2.\n"
79 "To be compatible with gNotify the following switch is accepted:\n"
80 " -t,--title Does nothing. Any text following will be treated as the\n"
81 " title because that's the default argument behaviour\n";
83 static const char *version = "growlnotify 1.1.5\n"
84 "Copyright (c) The Growl Project, 2004-2008";
86 static void notificationDismissed(CFNotificationCenterRef center,
90 CFDictionaryRef userInfo) {
91 #pragma unused(center,observer,name,object,userInfo)
92 CFRunLoopStop(CFRunLoopGetCurrent());
95 static CFDataRef copyIconDataForTypeInfo(CFStringRef typeInfo)
98 CFDataRef data = NULL;
99 OSStatus err = GetIconRefFromTypeInfo(/*inCreator*/ 0,
101 /*inExtension*/ typeInfo,
103 /*inUsageFlags*/kIconServicesNormalUsageFlag,
104 /*outIconRef*/ &icon);
106 IconFamilyHandle fam = NULL;
107 err = IconRefToIconFamily(icon, kSelectorAllAvailableData, &fam);
110 data = CFDataCreate(kCFAllocatorDefault, (const UInt8 *)*(Handle)fam, GetHandleSize((Handle)fam));
111 HUnlock((Handle)fam);
112 DisposeHandle((Handle)fam);
114 ReleaseIconRef(icon);
120 int main(int argc, const char **argv) {
127 char *appName = NULL;
128 char *appIcon = NULL;
129 char *iconExt = NULL;
130 char *iconPath = NULL;
131 char *imagePath = NULL;
132 char *message = NULL;
136 BOOL haveProgress = NO;
141 int code = EXIT_SUCCESS;
142 char *password = NULL;
143 char *identifier = NULL;
144 enum GrowlAuthenticationMethod authMethod = GROWL_AUTH_MD5;
146 struct option longopts[] = {
147 { "help", no_argument, NULL, 'h' },
148 { "name", required_argument, NULL, 'n' },
149 { "icon", required_argument, NULL, 'i' },
150 { "iconpath", required_argument, NULL, 'I' },
151 { "appIcon", required_argument, NULL, 'a' },
152 { "image", required_argument, &flag, 1 },
153 { "title", no_argument, NULL, 't' },
154 { "message", required_argument, NULL, 'm' },
155 { "priority", required_argument, NULL, 'p' },
156 { "host", required_argument, NULL, 'H' },
157 { "udp", no_argument, NULL, 'u' },
158 { "password", required_argument, NULL, 'P' },
159 { "port", required_argument, &flag, 2 },
160 { "version", no_argument, NULL, 'v' },
161 { "identifier", required_argument, NULL, 'd' },
162 { "wait", no_argument, NULL, 'w' },
163 { "auth", required_argument, NULL, 'A' },
164 { "crypt", no_argument, NULL, 'c' },
165 { "sticky", no_argument, NULL, 's' },
166 { "progress", required_argument, &flag, 3 },
167 { "html", no_argument, &flag, 4 },
171 while ((ch = getopt_long(argc, (char * const *)argv, "hvn:sa:A:i:I:p:tm:H:uP:d:wc", longopts, NULL)) != -1) {
201 if (sscanf(optarg, "%d", &priority) == 0) {
202 // It's not an integer - is it one of the priority keys?
203 char *keys[] = {"Very Low", "Moderate", "Normal", "High", "Emergency"};
204 for (int i = 0; i < 5; i++) {
205 if (strcmp(optarg, keys[i]) == 0) {
213 if (!strcasecmp(optarg, "md5"))
214 authMethod = GROWL_AUTH_MD5;
215 else if (!strcasecmp(optarg, "sha256"))
216 authMethod = GROWL_AUTH_SHA256;
217 else if (!strcasecmp(optarg, "none"))
218 authMethod = GROWL_AUTH_NONE;
220 fprintf(stderr, "Unknown digest algorithm: %s, using default (md5).\n", optarg);
252 port = strdup(optarg);
256 progress = strtod(optarg, NULL);
268 CFMutableStringRef title = CFStringCreateMutable(kCFAllocatorDefault, 0);
271 if (CFStringGetLength(title))
272 CFStringAppend(title, CFSTR(" "));
273 CFStringAppendCString(title, (argv++)[0], kCFStringEncodingUTF8);
277 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
280 // --image takes precedence over -I takes precedence over -i takes precedence over -a
281 CFDataRef icon = NULL;
283 // read the image file into a CFDataRef
284 icon = (CFDataRef)readFile(imagePath);
285 } else if (iconPath) {
286 // get icon data for path
287 NSString *path = [[NSString stringWithUTF8String:iconPath] stringByStandardizingPath];
288 if (![path isAbsolutePath])
289 path = [[[NSFileManager defaultManager] currentDirectoryPath] stringByAppendingPathComponent:path];
290 icon = (CFDataRef)copyIconDataForPath(path);
291 } else if (iconExt) {
292 // get icon data for file extension or type
293 CFStringRef fileType = CFStringCreateWithCString(kCFAllocatorDefault, iconExt, kCFStringEncodingUTF8);
294 icon = copyIconDataForTypeInfo(fileType);
296 } else if (appIcon) {
297 // get icon data for application name
298 CFStringRef app = CFStringCreateWithCString(kCFAllocatorDefault, appIcon, kCFStringEncodingUTF8);
299 NSURL *appURL = [NSURL fileURLWithPath:[[NSWorkspace sharedWorkspace] fullPathForApplication:(NSString *)app]];
301 icon = (CFDataRef)copyIconDataForURL(appURL);
306 NSURL *appURL = [NSURL fileURLWithPath:[[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:@"com.apple.Terminal"]];
308 icon = (CFDataRef)copyIconDataForURL((NSURL *)appURL);
314 if (message && !(message[0] == '-' && message[1] == 0)) {
316 desc = CFStringCreateWithCString(kCFAllocatorDefault, message, kCFStringEncodingUTF8);
319 if (isatty(STDIN_FILENO) && isatty(STDOUT_FILENO))
320 fputs("Enter a notification description, followed by newline, followed by Ctrl-D (End of File). To cancel, press Ctrl-C.\n", stdout);
323 CFMutableStringRef temp = CFStringCreateMutable(kCFAllocatorDefault, 0);
324 while (!feof(stdin)) {
325 size_t len = fread(buffer, 1, sizeof(buffer)-1, stdin);
329 CFStringAppendCString(temp, buffer, kCFStringEncodingUTF8);
331 CFStringTrimWhitespace(temp);
336 CFStringRef applicationName;
338 applicationName = CFStringCreateWithCString(kCFAllocatorDefault, appName, kCFStringEncodingUTF8);
340 applicationName = CFSTR("growlnotify");
342 CFStringRef identifierString;
344 identifierString = CFStringCreateWithCString(kCFAllocatorDefault, identifier, kCFStringEncodingUTF8);
346 identifierString = NULL;
348 // Register with Growl
349 CFStringRef name = NOTIFICATION_NAME;
350 CFArrayRef defaultAndAllNotifications = CFArrayCreate(kCFAllocatorDefault, (const void **)&name, 1, &kCFTypeArrayCallBacks);
351 CFTypeRef registerKeys[4] = {
353 GROWL_NOTIFICATIONS_ALL,
354 GROWL_NOTIFICATIONS_DEFAULT,
357 CFTypeRef registerValues[4] = {
359 defaultAndAllNotifications,
360 defaultAndAllNotifications,
363 CFDictionaryRef registerInfo = CFDictionaryCreate(kCFAllocatorDefault, registerKeys, registerValues, 4, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
364 CFRelease(defaultAndAllNotifications);
368 CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
369 CFStringRef clickContext = CFUUIDCreateString(kCFAllocatorDefault, uuid);
370 CFNumberRef priorityNumber = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &priority);
371 CFBooleanRef stickyValue = isSticky ? kCFBooleanTrue : kCFBooleanFalse;
372 CFMutableDictionaryRef notificationInfo = CFDictionaryCreateMutable(kCFAllocatorDefault ,9, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
373 CFDictionarySetValue(notificationInfo, GROWL_NOTIFICATION_NAME, name);
374 CFDictionarySetValue(notificationInfo, GROWL_APP_NAME, applicationName);
375 CFDictionarySetValue(notificationInfo, GROWL_NOTIFICATION_TITLE, title);
376 CFDictionarySetValue(notificationInfo, GROWL_NOTIFICATION_DESCRIPTION, desc);
377 CFDictionarySetValue(notificationInfo, GROWL_NOTIFICATION_PRIORITY, priorityNumber);
378 CFDictionarySetValue(notificationInfo, GROWL_NOTIFICATION_STICKY, stickyValue);
379 CFDictionarySetValue(notificationInfo, GROWL_NOTIFICATION_ICON, icon);
380 CFDictionarySetValue(notificationInfo, GROWL_NOTIFICATION_CLICK_CONTEXT, clickContext);
381 if (identifierString) {
382 CFDictionarySetValue(notificationInfo, GROWL_NOTIFICATION_IDENTIFIER, identifierString);
383 CFRelease(identifierString);
385 CFRelease(priorityNumber);
386 CFRelease(applicationName);
389 CFRelease(clickContext);
391 CFNumberRef progressNumber = CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &progress);
392 CFDictionarySetValue(notificationInfo, GROWL_NOTIFICATION_PROGRESS, progressNumber);
393 CFRelease(progressNumber);
398 NSLog(@"ERROR: Could not initialize CDSA.");
403 CSSM_DATA registrationPacket;
404 CSSM_DATA notificationPacket;
406 struct addrinfo hints;
409 memset(&hints, 0, sizeof(hints));
410 hints.ai_family = PF_UNSPEC;
411 hints.ai_socktype = SOCK_DGRAM;
412 hints.ai_protocol = IPPROTO_UDP;
413 error = getaddrinfo(host, port ? port : STRINGIFY(GROWL_UDP_PORT), &hints, &ai);
415 fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(error));
418 sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
423 registrationPacket.Data = GrowlUDPUtils_registrationToPacket((NSDictionary *)registerInfo,
426 (unsigned *)®istrationPacket.Length);
427 notificationPacket.Data = GrowlUDPUtils_notificationToPacket((NSDictionary *)notificationInfo,
430 (unsigned *)¬ificationPacket.Length);
432 CSSM_DATA passwordData;
433 passwordData.Data = (uint8 *)password;
435 passwordData.Length = strlen(password);
437 passwordData.Length = 0U;
439 GrowlUDPUtils_cryptPacket(®istrationPacket, CSSM_ALGID_AES, &passwordData, YES);
440 GrowlUDPUtils_cryptPacket(¬ificationPacket, CSSM_ALGID_AES, &passwordData, YES);
442 size = (registrationPacket.Length > notificationPacket.Length) ? registrationPacket.Length : notificationPacket.Length;
443 if (setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char *)&size, sizeof(size)) < 0)
444 perror("setsockopt: SO_SNDBUF");
446 if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (char *)&size, sizeof(size)) < 0)
447 perror("setsockopt: SO_BROADCAST");
449 //printf( "sendbuf: %d\n", size );
450 //printf( "registration packet length: %d\n", registrationPacket.Length );
451 //printf( "notification packet length: %d\n", notificationPacket.Length );
452 if (sendto(sock, registrationPacket.Data, registrationPacket.Length, 0, ai->ai_addr, ai->ai_addrlen) < 0) {
456 if (sendto(sock, notificationPacket.Data, notificationPacket.Length, 0, ai->ai_addr, ai->ai_addrlen) < 0) {
460 free(registrationPacket.Data);
461 free(notificationPacket.Data);
467 NSSocketPort *port = [[NSSocketPort alloc] initRemoteWithTCPPort:GROWL_TCP_PORT host:[NSString stringWithCString:host]];
468 NSConnection *connection = [[NSConnection alloc] initWithReceivePort:nil sendPort:port];
469 CFStringRef passwordString;
471 passwordString = CFStringCreateWithCString(kCFAllocatorDefault, password, kCFStringEncodingUTF8);
473 passwordString = NULL;
475 MD5Authenticator *authenticator = [[MD5Authenticator alloc] initWithPassword:(NSString *)passwordString];
477 CFRelease(passwordString);
478 [connection setDelegate:authenticator];
480 NSDistantObject *theProxy = [connection rootProxy];
481 [theProxy setProtocolForProxy:@protocol(GrowlNotificationProtocol)];
482 id<GrowlNotificationProtocol> growlProxy = (id)theProxy;
484 [growlProxy registerApplicationWithDictionary:(NSDictionary *)registerInfo];
485 [growlProxy postNotificationWithDictionary:(NSDictionary *)notificationInfo];
486 } @catch(NSException *e) {
487 if ([[e name] isEqualToString:NSFailedAuthenticationException])
488 NSLog(@"Authentication failed");
490 NSLog(@"Exception: %@", [e name]);
493 [connection release];
494 [authenticator release];
499 CFNotificationCenterRef distCenter = CFNotificationCenterGetDistributedCenter();
501 CFMutableStringRef notificationName = CFStringCreateMutable(kCFAllocatorDefault, 0);
502 CFStringAppend(notificationName, applicationName);
503 CFStringAppend(notificationName, (CFStringRef)GROWL_NOTIFICATION_CLICKED);
504 CFNotificationCenterAddObserver(distCenter,
506 notificationDismissed,
509 CFNotificationSuspensionBehaviorCoalesce);
510 CFStringReplaceAll(notificationName, applicationName);
511 CFStringAppend(notificationName, (CFStringRef)GROWL_NOTIFICATION_TIMED_OUT);
512 CFNotificationCenterAddObserver(distCenter,
514 notificationDismissed,
517 CFNotificationSuspensionBehaviorCoalesce);
518 CFRelease(notificationName);
521 NSConnection *connection = [NSConnection connectionWithRegisteredName:@"GrowlApplicationBridgePathway" host:nil];
523 //Post to Growl via GrowlApplicationBridgePathway
525 NSDistantObject *theProxy = [connection rootProxy];
526 [theProxy setProtocolForProxy:@protocol(GrowlNotificationProtocol)];
527 id<GrowlNotificationProtocol> growlProxy = (id)theProxy;
528 [growlProxy registerApplicationWithDictionary:(NSDictionary *)registerInfo];
529 [growlProxy postNotificationWithDictionary:(NSDictionary *)notificationInfo];
530 } @catch(NSException *e) {
531 NSLog(@"exception while sending notification: %@", e);
534 //Post to Growl via NSDistributedNotificationCenter
535 NSLog(@"could not find local GrowlApplicationBridgePathway, falling back to NSDNC");
536 CFNotificationCenterPostNotificationWithOptions(distCenter, (CFStringRef)GROWL_APP_REGISTRATION, NULL, registerInfo, kCFNotificationPostToAllSessions);
537 CFNotificationCenterPostNotificationWithOptions(distCenter, (CFStringRef)GROWL_NOTIFICATION, NULL, notificationInfo, kCFNotificationPostToAllSessions);
541 /* Run the run loop until it is manually cancelled in notificationDismissed() */
544 /* Run the run loop until we don't have any sources to proces
545 * to ensure the distributed notification is posted */
546 while (CFRunLoopRunInMode(/* mode */ kCFRunLoopDefaultMode,
547 /* seconds; 0 means single iteration */ 0,
548 /* returnAfterSourceHandled */ TRUE) == kCFRunLoopRunHandledSource);
555 CFRelease(registerInfo);
556 CFRelease(notificationInfo);