1 /*
2 Platypus - create MacOS X application bundles that execute scripts
3 This is the executable that goes into Platypus apps
4 Copyright (C) 2003 Sveinbjorn Thordarson <sveinbt@hi.is>
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 main.c - main program file
22 */
24 ///////////////////////////////////////
25 // Includes
26 ///////////////////////////////////////
27 #pragma mark Includes
29 // Apple stuff
30 #include <Carbon/Carbon.h>
31 #include <CoreFoundation/CoreFoundation.h>
32 #include <Security/Authorization.h>
33 #include <Security/AuthorizationTags.h>
35 // Unix stuff
36 #include <string.h>
37 #include <unistd.h>
38 #include <sys/wait.h>
39 #include <pthread.h>
40 #include <stdio.h>
42 ///////////////////////////////////////
43 // Definitions
44 ///////////////////////////////////////
45 #pragma mark Definitions
47 // name length limits
48 #define kMaxPathLength 1024
50 // names of files bundled with app
51 #define kScriptFileName "script"
52 #define kOpenDocFileName "openDoc"
54 // custom carbon events
55 #define kEventClassRedFatalAlert 911
56 #define kEventKindX11Failed 911
57 #define kEventKindFCCacheFailed 912
59 //maximum arguments the script accepts
60 #define kMaxArgumentsToScript 252
62 ///////////////////////////////////////
63 // Prototypes
64 ///////////////////////////////////////
65 #pragma mark Prototypes
67 static void *Execute(void *arg);
68 static void *OpenDoc(void *arg);
69 static OSErr ExecuteScript(char *script, pid_t *pid);
71 static void GetParameters(void);
72 static char* GetScript(void);
73 static char* GetOpenDoc(void);
75 OSErr LoadMenuBar(char *appName);
77 static OSStatus FSMakePath(FSSpec file, char *path, long maxPathSize);
78 static void RedFatalAlert(Str255 errorString, Str255 expStr);
79 static short DoesFileExist(char *path);
80 static OSStatus FixFCCache(void);
82 static OSErr AppQuitAEHandler(const AppleEvent *theAppleEvent,
83 AppleEvent *reply, long refCon);
84 static OSErr AppOpenDocAEHandler(const AppleEvent *theAppleEvent,
85 AppleEvent *reply, long refCon);
86 static OSErr AppOpenAppAEHandler(const AppleEvent *theAppleEvent,
87 AppleEvent *reply, long refCon);
88 static OSStatus X11FailedHandler(EventHandlerCallRef theHandlerCall,
89 EventRef theEvent, void *userData);
90 static OSStatus FCCacheFailedHandler(EventHandlerCallRef theHandlerCall,
91 EventRef theEvent, void *userData);
92 ///////////////////////////////////////
93 // Globals
94 ///////////////////////////////////////
95 #pragma mark Globals
97 // process id of forked process
98 pid_t pid = 0;
100 // thread id of threads that start scripts
101 pthread_t odtid = 0, tid = 0;
103 // indicator of whether the script has completed executing
104 short taskDone = true;
106 // execution parameters
107 char scriptPath[kMaxPathLength];
108 char openDocPath[kMaxPathLength];
110 //arguments to the script
111 char *arguments[kMaxArgumentsToScript+3];
112 char *fileArgs[kMaxArgumentsToScript];
113 short numArgs = 0;
115 extern char **environ;
117 #pragma mark -
119 ///////////////////////////////////////
120 // Program entrance point
121 ///////////////////////////////////////
122 int main(int argc, char* argv[])
123 {
124 OSErr err = noErr;
125 EventTypeSpec X11events = { kEventClassRedFatalAlert, kEventKindX11Failed };
126 EventTypeSpec FCCacheEvents = { kEventClassRedFatalAlert, kEventKindFCCacheFailed };
128 InitCursor();
130 //install Apple Event handlers
131 err += AEInstallEventHandler(kCoreEventClass, kAEQuitApplication,
132 NewAEEventHandlerUPP(AppQuitAEHandler),
133 0, false);
134 err += AEInstallEventHandler(kCoreEventClass, kAEOpenDocuments,
135 NewAEEventHandlerUPP(AppOpenDocAEHandler),
136 0, false);
137 err += AEInstallEventHandler(kCoreEventClass, kAEOpenApplication,
138 NewAEEventHandlerUPP(AppOpenAppAEHandler),
139 0, false);
140 err += InstallEventHandler(GetApplicationEventTarget(),
141 NewEventHandlerUPP(X11FailedHandler), 1,
142 &X11events, NULL, NULL);
143 err += InstallEventHandler(GetApplicationEventTarget(),
144 NewEventHandlerUPP(FCCacheFailedHandler), 1,
145 &FCCacheEvents, NULL, NULL);
147 if (err) RedFatalAlert("\pInitialization Error",
148 "\pError initing Apple Event handlers.");
150 //create the menu bar
151 if (err = LoadMenuBar(NULL)) RedFatalAlert("\pInitialization Error",
152 "\pError loading MenuBar.nib.");
154 GetParameters(); //load data from files containing exec settings
156 RunApplicationEventLoop(); //Run the event loop
157 return 0;
158 }
160 #pragma mark -
163 static void RequestUserAttention(void)
164 {
165 NMRecPtr notificationRequest = (NMRecPtr) NewPtr(sizeof(NMRec));
167 memset(notificationRequest, 0, sizeof(*notificationRequest));
168 notificationRequest->qType = nmType;
169 notificationRequest->nmMark = 1;
170 notificationRequest->nmIcon = 0;
171 notificationRequest->nmSound = 0;
172 notificationRequest->nmStr = NULL;
173 notificationRequest->nmResp = NULL;
175 verify_noerr(NMInstall(notificationRequest));
176 }
179 static void ShowFirstStartWarningDialog(void)
180 {
181 SInt16 itemHit;
183 AlertStdAlertParamRec params;
184 params.movable = true;
185 params.helpButton = false;
186 params.filterProc = NULL;
187 params.defaultText = (void *) kAlertDefaultOKText;
188 params.cancelText = NULL;
189 params.otherText = NULL;
190 params.defaultButton = kAlertStdAlertOKButton;
191 params.cancelButton = kAlertStdAlertCancelButton;
192 params.position = kWindowDefaultPosition;
194 StandardAlert(kAlertNoteAlert, "\pInkscape on Mac OS X",
195 "\pWhile Inkscape is open, its windows can be displayed or hidden by displaying or hiding the X11 application.\n\nThe first time this version of Inkscape is run it may take several minutes before the main window is displayed while font caches are built.",
196 ¶ms, &itemHit);
197 }
200 //////////////////////////////////
201 // Handler for when fontconfig caches need to be generated
202 //////////////////////////////////
203 static OSStatus FCCacheFailedHandler(EventHandlerCallRef theHandlerCall,
204 EventRef theEvent, void *userData)
205 {
207 pthread_join(tid, NULL);
208 if (odtid) pthread_join(odtid, NULL);
210 // Bounce Inkscape Dock icon
211 RequestUserAttention();
212 // Need to show warning to the user, then carry on.
213 ShowFirstStartWarningDialog();
215 // Note that we've seen the warning.
216 system("test -d \"$HOME/.inkscape\" || mkdir \"$HOME/.inkscape\"; "
217 "touch \"$HOME/.inkscape/.fccache-new\"");
218 // Rerun now.
219 OSErr err = ExecuteScript(scriptPath, &pid);
221 return err;
222 }
225 static size_t safeRead(int d, void *buf, size_t nbytes)
226 {
227 ssize_t bytesToRead = nbytes;
228 ssize_t bytesRead = 0;
229 char *offset = (char *) buf;
231 while ((bytesToRead > 0))
232 {
233 bytesRead = read(d, offset, bytesToRead);
234 if (bytesRead > 0)
235 {
236 offset += bytesRead;
237 bytesToRead -= bytesRead;
238 }
239 else if (bytesRead == 0)
240 {
241 // Reached EOF.
242 break;
243 }
244 else if (bytesRead == -1)
245 {
246 if ((errno == EINTR) || (errno == EAGAIN))
247 {
248 // Try again.
249 continue;
250 }
251 return 0;
252 }
253 }
254 return bytesRead;
255 }
258 /////////////////////////////////////
259 // Code to run fc-cache on first run
260 /////////////////////////////////////
261 static OSStatus FixFCCache (void)
262 {
263 FILE *fileConnToChild = NULL;
264 int fdConnToChild = 0;
265 pid_t childPID = WAIT_ANY;
266 size_t bytesChildPID;
267 size_t bytesRead;
268 int status;
270 char commandStr[] = "/usr/X11R6/bin/fc-cache";
271 char *commandArgs[] = { "-f", NULL };
273 // Run fc-cache
274 AuthorizationItem authItems[] =
275 {
276 {
277 kAuthorizationRightExecute,
278 strlen(commandStr),
279 commandStr,
280 0
281 }
282 };
283 AuthorizationItemSet authItemSet =
284 {
285 1,
286 authItems
287 };
288 AuthorizationRef authRef = NULL;
289 OSStatus err = AuthorizationCreate (NULL, &authItemSet,
290 kAuthorizationFlagInteractionAllowed |
291 kAuthorizationFlagExtendRights, &authRef);
293 if (err == errAuthorizationSuccess)
294 {
295 err = AuthorizationExecuteWithPrivileges(authRef, commandStr,
296 kAuthorizationFlagDefaults, commandArgs,
297 &fileConnToChild);
299 if (err == errAuthorizationSuccess)
300 {
301 // Unfortunately, AuthorizationExecuteWithPrivileges
302 // does not return the process ID associated with the
303 // process it runs. The best solution we have it to
304 // try and get the process ID from the file descriptor.
305 // This is based on example code from Apple's
306 // MoreAuthSample.
308 fdConnToChild = fileno(fileConnToChild);
310 // Try an get the process ID of the fc-cache command
311 bytesChildPID = sizeof(childPID);
312 bytesRead = safeRead(fdConnToChild, &childPID,
313 bytesChildPID);
314 if (bytesRead != bytesChildPID)
315 {
316 // If we can't get it the best alternative
317 // is to wait for any child to finish.
318 childPID = WAIT_ANY;
319 }
321 if (fileConnToChild != NULL) {
322 fclose(fileConnToChild);
323 }
325 // Wait for child process to finish.
326 waitpid(childPID, &status, 0);
327 }
328 }
329 AuthorizationFree(authRef, kAuthorizationFlagDestroyRights);
331 return err;
332 }
335 ///////////////////////////////////
336 // Execution thread starts here
337 ///////////////////////////////////
338 static void *Execute (void *arg)
339 {
340 EventRef event;
342 taskDone = false;
344 OSErr err = ExecuteScript(scriptPath, &pid);
345 if (err == (OSErr)11) {
346 CreateEvent(NULL, kEventClassRedFatalAlert, kEventKindX11Failed, 0,
347 kEventAttributeNone, &event);
348 PostEventToQueue(GetMainEventQueue(), event, kEventPriorityStandard);
349 }
350 else if (err == (OSErr)12) {
351 CreateEvent(NULL, kEventClassRedFatalAlert, kEventKindFCCacheFailed, 0,
352 kEventAttributeNone, &event);
353 PostEventToQueue(GetMainEventQueue(), event, kEventPriorityHigh);
354 }
355 else ExitToShell();
356 return 0;
357 }
359 ///////////////////////////////////
360 // Open additional documents thread starts here
361 ///////////////////////////////////
362 static void *OpenDoc (void *arg)
363 {
364 ExecuteScript(openDocPath, NULL);
365 return 0;
366 }
368 ///////////////////////////////////////
369 // Run a script via the system command
370 ///////////////////////////////////////
371 static OSErr ExecuteScript (char *script, pid_t *pid)
372 {
373 pid_t wpid = 0, p = 0;
374 int status, i;
376 if (! pid) pid = &p;
378 // Generate the array of argument strings before we do any executing
379 arguments[0] = script;
380 for (i = 0; i < numArgs; i++) arguments[i + 1] = fileArgs[i];
381 arguments[i + 1] = NULL;
383 *pid = fork(); //open fork
385 if (*pid == (pid_t)-1) exit(13); //error
386 else if (*pid == 0) { //child process started
387 execve(arguments[0], arguments, environ);
388 exit(13); //if we reach this point, there's an error
389 }
391 wpid = waitpid(*pid, &status, 0); //wait while child process finishes
393 if (wpid == (pid_t)-1) return wpid;
394 return (OSErr)WEXITSTATUS(status);
395 }
397 #pragma mark -
399 ///////////////////////////////////////
400 // This function loads all the neccesary settings
401 // from config files in the Resources folder
402 ///////////////////////////////////////
403 static void GetParameters (void)
404 {
405 char *str;
406 if (! (str = (char *)GetScript())) //get path to script to be executed
407 RedFatalAlert("\pInitialization Error",
408 "\pError getting script from application bundle.");
409 strcpy((char *)&scriptPath, str);
411 if (! (str = (char *)GetOpenDoc())) //get path to openDoc
412 RedFatalAlert("\pInitialization Error",
413 "\pError getting openDoc from application bundle.");
414 strcpy((char *)&openDocPath, str);
415 }
417 ///////////////////////////////////////
418 // Get path to the script in Resources folder
419 ///////////////////////////////////////
420 static char* GetScript (void)
421 {
422 CFStringRef fileName;
423 CFBundleRef appBundle;
424 CFURLRef scriptFileURL;
425 FSRef fileRef;
426 FSSpec fileSpec;
427 char *path;
429 //get CF URL for script
430 if (! (appBundle = CFBundleGetMainBundle())) return NULL;
431 if (! (fileName = CFStringCreateWithCString(NULL, kScriptFileName,
432 kCFStringEncodingASCII)))
433 return NULL;
434 if (! (scriptFileURL = CFBundleCopyResourceURL(appBundle, fileName, NULL,
435 NULL))) return NULL;
437 //Get file reference from Core Foundation URL
438 if (! CFURLGetFSRef(scriptFileURL, &fileRef)) return NULL;
440 //dispose of the CF variables
441 CFRelease(scriptFileURL);
442 CFRelease(fileName);
444 //convert FSRef to FSSpec
445 if (FSGetCatalogInfo(&fileRef, kFSCatInfoNone, NULL, NULL, &fileSpec,
446 NULL)) return NULL;
448 //create path string
449 if (! (path = malloc(kMaxPathLength))) return NULL;
450 if (FSMakePath(fileSpec, path, kMaxPathLength)) return NULL;
451 if (! DoesFileExist(path)) return NULL;
453 return path;
454 }
456 ///////////////////////////////////////
457 // Gets the path to openDoc in Resources folder
458 ///////////////////////////////////////
459 static char* GetOpenDoc (void)
460 {
461 CFStringRef fileName;
462 CFBundleRef appBundle;
463 CFURLRef openDocFileURL;
464 FSRef fileRef;
465 FSSpec fileSpec;
466 char *path;
468 //get CF URL for openDoc
469 if (! (appBundle = CFBundleGetMainBundle())) return NULL;
470 if (! (fileName = CFStringCreateWithCString(NULL, kOpenDocFileName,
471 kCFStringEncodingASCII)))
472 return NULL;
473 if (! (openDocFileURL = CFBundleCopyResourceURL(appBundle, fileName, NULL,
474 NULL))) return NULL;
476 //Get file reference from Core Foundation URL
477 if (! CFURLGetFSRef( openDocFileURL, &fileRef )) return NULL;
479 //dispose of the CF variables
480 CFRelease(openDocFileURL);
481 CFRelease(fileName);
483 //convert FSRef to FSSpec
484 if (FSGetCatalogInfo(&fileRef, kFSCatInfoNone, NULL, NULL, &fileSpec,
485 NULL)) return NULL;
487 //create path string
488 if (! (path = malloc(kMaxPathLength))) return NULL;
489 if (FSMakePath(fileSpec, path, kMaxPathLength)) return NULL;
490 if (! DoesFileExist(path)) return NULL;
492 return path;
493 }
495 #pragma mark -
497 /////////////////////////////////////
498 // Load menu bar from nib
499 /////////////////////////////////////
500 OSErr LoadMenuBar (char *appName)
501 {
502 OSErr err;
503 IBNibRef nibRef;
505 if (err = CreateNibReference(CFSTR("MenuBar"), &nibRef)) return err;
506 if (err = SetMenuBarFromNib(nibRef, CFSTR("MenuBar"))) return err;
507 DisposeNibReference(nibRef);
509 return noErr;
510 }
512 #pragma mark -
514 ///////////////////////////////////////
515 // Generate path string from FSSpec record
516 ///////////////////////////////////////
517 static OSStatus FSMakePath(FSSpec file, char *path, long maxPathSize)
518 {
519 OSErr err = noErr;
520 FSRef fileRef;
522 //create file reference from file spec
523 if (err = FSpMakeFSRef(&file, &fileRef)) return err;
525 // and then convert the FSRef to a path
526 return FSRefMakePath(&fileRef, path, maxPathSize);
527 }
529 ////////////////////////////////////////
530 // Standard red error alert, then exit application
531 ////////////////////////////////////////
532 static void RedFatalAlert (Str255 errorString, Str255 expStr)
533 {
534 StandardAlert(kAlertStopAlert, errorString, expStr, NULL, NULL);
535 ExitToShell();
536 }
538 ///////////////////////////////////////
539 // Determines whether file exists at path or not
540 ///////////////////////////////////////
541 static short DoesFileExist (char *path)
542 {
543 if (access(path, F_OK) == -1) return false;
544 return true;
545 }
547 #pragma mark -
549 ///////////////////////////////////////
550 // Apple Event handler for Quit i.e. from
551 // the dock or Application menu item
552 ///////////////////////////////////////
553 static OSErr AppQuitAEHandler(const AppleEvent *theAppleEvent,
554 AppleEvent *reply, long refCon)
555 {
556 #pragma unused (reply, refCon, theAppleEvent)
558 while (numArgs > 0) free(fileArgs[numArgs--]);
560 if (! taskDone && pid) { //kill the script process brutally
561 kill(pid, 9);
562 printf("Platypus App: PID %d killed brutally\n", pid);
563 }
565 pthread_cancel(tid);
566 if (odtid) pthread_cancel(odtid);
568 ExitToShell();
570 return noErr;
571 }
573 /////////////////////////////////////
574 // Handler for docs dragged on app icon
575 /////////////////////////////////////
576 static OSErr AppOpenDocAEHandler(const AppleEvent *theAppleEvent,
577 AppleEvent *reply, long refCon)
578 {
579 #pragma unused (reply, refCon)
581 OSErr err = noErr;
582 AEDescList fileSpecList;
583 AEKeyword keyword;
584 DescType type;
586 short i;
587 long count, actualSize;
589 FSSpec fileSpec;
590 char path[kMaxPathLength];
592 while (numArgs > 0) free(fileArgs[numArgs--]);
594 //Read the AppleEvent
595 err = AEGetParamDesc(theAppleEvent, keyDirectObject, typeAEList,
596 &fileSpecList);
598 err = AECountItems(&fileSpecList, &count); //Count number of files
600 for (i = 1; i <= count; i++) { //iteratively process each file
601 //get fsspec from apple event
602 if (! (err = AEGetNthPtr(&fileSpecList, i, typeFSS, &keyword, &type,
603 (Ptr)&fileSpec, sizeof(FSSpec), &actualSize)))
604 {
605 //get path from file spec
606 if ((err = FSMakePath(fileSpec, (unsigned char *)&path,
607 kMaxPathLength))) return err;
609 if (numArgs == kMaxArgumentsToScript) break;
611 if (! (fileArgs[numArgs] = malloc(kMaxPathLength))) return true;
613 strcpy(fileArgs[numArgs++], (char *)&path);
614 }
615 else return err;
616 }
618 if (! taskDone) pthread_create(&odtid, NULL, OpenDoc, NULL);
619 else pthread_create(&tid, NULL, Execute, NULL);
621 return err;
622 }
624 ///////////////////////////////
625 // Handler for clicking on app icon
626 ///////////////////////////////
627 static OSErr AppOpenAppAEHandler(const AppleEvent *theAppleEvent,
628 AppleEvent *reply, long refCon)
629 {
630 #pragma unused (reply, refCon, theAppleEvent)
632 // the app has been opened without any items dragged on to it
633 pthread_create(&tid, NULL, Execute, NULL);
635 return noErr;
636 }
639 static void OpenURL(Str255 url)
640 {
641 // Use Internet Config to hand the URL to the appropriate application, as
642 // set by the user in the Internet Preferences pane.
643 ICInstance icInstance;
644 // Applications creator code:
645 OSType signature = 'Inks';
646 OSStatus error = ICStart( &icInstance, signature );
647 if ( error == noErr )
648 {
649 ConstStr255Param hint = 0x0;
650 const char* data = url;
651 long length = strlen(url);
652 long start = 0;
653 long end = length;
654 // Don't bother testing return value (error); launched application will
655 // report problems.
656 ICLaunchURL( icInstance, hint, data, length, &start, &end );
657 ICStop( icInstance );
658 }
659 }
662 //////////////////////////////////
663 // Handler for when X11 fails to start
664 //////////////////////////////////
665 static OSStatus X11FailedHandler(EventHandlerCallRef theHandlerCall,
666 EventRef theEvent, void *userData)
667 {
668 #pragma unused(theHanderCall, theEvent, userData)
670 pthread_join(tid, NULL);
671 if (odtid) pthread_join(odtid, NULL);
673 SInt16 itemHit;
674 const char *getX11 = "\pGet X11 for Panther";
676 AlertStdAlertParamRec params;
677 params.movable = true;
678 params.helpButton = false;
679 params.filterProc = NULL;
680 params.defaultText = (StringPtr) kAlertDefaultOKText;
681 params.cancelText = getX11;
682 params.otherText = NULL;
683 params.defaultButton = kAlertStdAlertOKButton;
684 params.cancelButton = kAlertStdAlertCancelButton;
685 params.position = kWindowDefaultPosition;
687 StandardAlert(kAlertStopAlert, "\pFailed to start X11",
688 "\pInkscape.app requires Apple's X11, which is freely downloadable from Apple's website for Panther (10.3.x) users and available as an optional install from the installation DVD for Tiger (10.4.x) users.\n\nPlease install X11 and restart Inkscape.",
689 ¶ms, &itemHit);
691 if (itemHit == kAlertStdAlertCancelButton)
692 {
693 OpenURL("http://www.apple.com/downloads/macosx/apple/x11formacosx.html");
694 }
696 ExitToShell();
699 return noErr;
700 }