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);
220 ExitToShell();
222 return noErr;
223 }
226 static size_t safeRead(int d, void *buf, size_t nbytes)
227 {
228 ssize_t bytesToRead = nbytes;
229 ssize_t bytesRead = 0;
230 char *offset = (char *) buf;
232 while ((bytesToRead > 0))
233 {
234 bytesRead = read(d, offset, bytesToRead);
235 if (bytesRead > 0)
236 {
237 offset += bytesRead;
238 bytesToRead -= bytesRead;
239 }
240 else if (bytesRead == 0)
241 {
242 // Reached EOF.
243 break;
244 }
245 else if (bytesRead == -1)
246 {
247 if ((errno == EINTR) || (errno == EAGAIN))
248 {
249 // Try again.
250 continue;
251 }
252 return 0;
253 }
254 }
255 return bytesRead;
256 }
259 /////////////////////////////////////
260 // Code to run fc-cache on first run
261 /////////////////////////////////////
262 static OSStatus FixFCCache (void)
263 {
264 FILE *fileConnToChild = NULL;
265 int fdConnToChild = 0;
266 pid_t childPID = WAIT_ANY;
267 size_t bytesChildPID;
268 size_t bytesRead;
269 int status;
271 char commandStr[] = "/usr/X11R6/bin/fc-cache";
272 char *commandArgs[] = { "-f", NULL };
274 // Run fc-cache
275 AuthorizationItem authItems[] =
276 {
277 {
278 kAuthorizationRightExecute,
279 strlen(commandStr),
280 commandStr,
281 0
282 }
283 };
284 AuthorizationItemSet authItemSet =
285 {
286 1,
287 authItems
288 };
289 AuthorizationRef authRef = NULL;
290 OSStatus err = AuthorizationCreate (NULL, &authItemSet,
291 kAuthorizationFlagInteractionAllowed |
292 kAuthorizationFlagExtendRights, &authRef);
294 if (err == errAuthorizationSuccess)
295 {
296 err = AuthorizationExecuteWithPrivileges(authRef, commandStr,
297 kAuthorizationFlagDefaults, commandArgs,
298 &fileConnToChild);
300 if (err == errAuthorizationSuccess)
301 {
302 // Unfortunately, AuthorizationExecuteWithPrivileges
303 // does not return the process ID associated with the
304 // process it runs. The best solution we have it to
305 // try and get the process ID from the file descriptor.
306 // This is based on example code from Apple's
307 // MoreAuthSample.
309 fdConnToChild = fileno(fileConnToChild);
311 // Try an get the process ID of the fc-cache command
312 bytesChildPID = sizeof(childPID);
313 bytesRead = safeRead(fdConnToChild, &childPID,
314 bytesChildPID);
315 if (bytesRead != bytesChildPID)
316 {
317 // If we can't get it the best alternative
318 // is to wait for any child to finish.
319 childPID = WAIT_ANY;
320 }
322 if (fileConnToChild != NULL) {
323 fclose(fileConnToChild);
324 }
326 // Wait for child process to finish.
327 waitpid(childPID, &status, 0);
328 }
329 }
330 AuthorizationFree(authRef, kAuthorizationFlagDestroyRights);
332 return err;
333 }
336 ///////////////////////////////////
337 // Execution thread starts here
338 ///////////////////////////////////
339 static void *Execute (void *arg)
340 {
341 EventRef event;
343 taskDone = false;
345 OSErr err = ExecuteScript(scriptPath, &pid);
346 if (err == (OSErr)11) {
347 CreateEvent(NULL, kEventClassRedFatalAlert, kEventKindX11Failed, 0,
348 kEventAttributeNone, &event);
349 PostEventToQueue(GetMainEventQueue(), event, kEventPriorityStandard);
350 }
351 else if (err == (OSErr)12) {
352 CreateEvent(NULL, kEventClassRedFatalAlert, kEventKindFCCacheFailed, 0,
353 kEventAttributeNone, &event);
354 PostEventToQueue(GetMainEventQueue(), event, kEventPriorityHigh);
355 }
356 else ExitToShell();
357 return 0;
358 }
360 ///////////////////////////////////
361 // Open additional documents thread starts here
362 ///////////////////////////////////
363 static void *OpenDoc (void *arg)
364 {
365 ExecuteScript(openDocPath, NULL);
366 return 0;
367 }
369 ///////////////////////////////////////
370 // Run a script via the system command
371 ///////////////////////////////////////
372 static OSErr ExecuteScript (char *script, pid_t *pid)
373 {
374 pid_t wpid = 0, p = 0;
375 int status, i;
377 if (! pid) pid = &p;
379 // Generate the array of argument strings before we do any executing
380 arguments[0] = script;
381 for (i = 0; i < numArgs; i++) arguments[i + 1] = fileArgs[i];
382 arguments[i + 1] = NULL;
384 *pid = fork(); //open fork
386 if (*pid == (pid_t)-1) exit(13); //error
387 else if (*pid == 0) { //child process started
388 execve(arguments[0], arguments, environ);
389 exit(13); //if we reach this point, there's an error
390 }
392 wpid = waitpid(*pid, &status, 0); //wait while child process finishes
394 if (wpid == (pid_t)-1) return wpid;
395 return (OSErr)WEXITSTATUS(status);
396 }
398 #pragma mark -
400 ///////////////////////////////////////
401 // This function loads all the neccesary settings
402 // from config files in the Resources folder
403 ///////////////////////////////////////
404 static void GetParameters (void)
405 {
406 char *str;
407 if (! (str = (char *)GetScript())) //get path to script to be executed
408 RedFatalAlert("\pInitialization Error",
409 "\pError getting script from application bundle.");
410 strcpy((char *)&scriptPath, str);
412 if (! (str = (char *)GetOpenDoc())) //get path to openDoc
413 RedFatalAlert("\pInitialization Error",
414 "\pError getting openDoc from application bundle.");
415 strcpy((char *)&openDocPath, str);
416 }
418 ///////////////////////////////////////
419 // Get path to the script in Resources folder
420 ///////////////////////////////////////
421 static char* GetScript (void)
422 {
423 CFStringRef fileName;
424 CFBundleRef appBundle;
425 CFURLRef scriptFileURL;
426 FSRef fileRef;
427 FSSpec fileSpec;
428 char *path;
430 //get CF URL for script
431 if (! (appBundle = CFBundleGetMainBundle())) return NULL;
432 if (! (fileName = CFStringCreateWithCString(NULL, kScriptFileName,
433 kCFStringEncodingASCII)))
434 return NULL;
435 if (! (scriptFileURL = CFBundleCopyResourceURL(appBundle, fileName, NULL,
436 NULL))) return NULL;
438 //Get file reference from Core Foundation URL
439 if (! CFURLGetFSRef(scriptFileURL, &fileRef)) return NULL;
441 //dispose of the CF variables
442 CFRelease(scriptFileURL);
443 CFRelease(fileName);
445 //convert FSRef to FSSpec
446 if (FSGetCatalogInfo(&fileRef, kFSCatInfoNone, NULL, NULL, &fileSpec,
447 NULL)) return NULL;
449 //create path string
450 if (! (path = malloc(kMaxPathLength))) return NULL;
451 if (FSMakePath(fileSpec, path, kMaxPathLength)) return NULL;
452 if (! DoesFileExist(path)) return NULL;
454 return path;
455 }
457 ///////////////////////////////////////
458 // Gets the path to openDoc in Resources folder
459 ///////////////////////////////////////
460 static char* GetOpenDoc (void)
461 {
462 CFStringRef fileName;
463 CFBundleRef appBundle;
464 CFURLRef openDocFileURL;
465 FSRef fileRef;
466 FSSpec fileSpec;
467 char *path;
469 //get CF URL for openDoc
470 if (! (appBundle = CFBundleGetMainBundle())) return NULL;
471 if (! (fileName = CFStringCreateWithCString(NULL, kOpenDocFileName,
472 kCFStringEncodingASCII)))
473 return NULL;
474 if (! (openDocFileURL = CFBundleCopyResourceURL(appBundle, fileName, NULL,
475 NULL))) return NULL;
477 //Get file reference from Core Foundation URL
478 if (! CFURLGetFSRef( openDocFileURL, &fileRef )) return NULL;
480 //dispose of the CF variables
481 CFRelease(openDocFileURL);
482 CFRelease(fileName);
484 //convert FSRef to FSSpec
485 if (FSGetCatalogInfo(&fileRef, kFSCatInfoNone, NULL, NULL, &fileSpec,
486 NULL)) return NULL;
488 //create path string
489 if (! (path = malloc(kMaxPathLength))) return NULL;
490 if (FSMakePath(fileSpec, path, kMaxPathLength)) return NULL;
491 if (! DoesFileExist(path)) return NULL;
493 return path;
494 }
496 #pragma mark -
498 /////////////////////////////////////
499 // Load menu bar from nib
500 /////////////////////////////////////
501 OSErr LoadMenuBar (char *appName)
502 {
503 OSErr err;
504 IBNibRef nibRef;
506 if (err = CreateNibReference(CFSTR("MenuBar"), &nibRef)) return err;
507 if (err = SetMenuBarFromNib(nibRef, CFSTR("MenuBar"))) return err;
508 DisposeNibReference(nibRef);
510 return noErr;
511 }
513 #pragma mark -
515 ///////////////////////////////////////
516 // Generate path string from FSSpec record
517 ///////////////////////////////////////
518 static OSStatus FSMakePath(FSSpec file, char *path, long maxPathSize)
519 {
520 OSErr err = noErr;
521 FSRef fileRef;
523 //create file reference from file spec
524 if (err = FSpMakeFSRef(&file, &fileRef)) return err;
526 // and then convert the FSRef to a path
527 return FSRefMakePath(&fileRef, path, maxPathSize);
528 }
530 ////////////////////////////////////////
531 // Standard red error alert, then exit application
532 ////////////////////////////////////////
533 static void RedFatalAlert (Str255 errorString, Str255 expStr)
534 {
535 StandardAlert(kAlertStopAlert, errorString, expStr, NULL, NULL);
536 ExitToShell();
537 }
539 ///////////////////////////////////////
540 // Determines whether file exists at path or not
541 ///////////////////////////////////////
542 static short DoesFileExist (char *path)
543 {
544 if (access(path, F_OK) == -1) return false;
545 return true;
546 }
548 #pragma mark -
550 ///////////////////////////////////////
551 // Apple Event handler for Quit i.e. from
552 // the dock or Application menu item
553 ///////////////////////////////////////
554 static OSErr AppQuitAEHandler(const AppleEvent *theAppleEvent,
555 AppleEvent *reply, long refCon)
556 {
557 #pragma unused (reply, refCon, theAppleEvent)
559 while (numArgs > 0) free(fileArgs[numArgs--]);
561 if (! taskDone && pid) { //kill the script process brutally
562 kill(pid, 9);
563 printf("Platypus App: PID %d killed brutally\n", pid);
564 }
566 pthread_cancel(tid);
567 if (odtid) pthread_cancel(odtid);
569 ExitToShell();
571 return noErr;
572 }
574 /////////////////////////////////////
575 // Handler for docs dragged on app icon
576 /////////////////////////////////////
577 static OSErr AppOpenDocAEHandler(const AppleEvent *theAppleEvent,
578 AppleEvent *reply, long refCon)
579 {
580 #pragma unused (reply, refCon)
582 OSErr err = noErr;
583 AEDescList fileSpecList;
584 AEKeyword keyword;
585 DescType type;
587 short i;
588 long count, actualSize;
590 FSSpec fileSpec;
591 char path[kMaxPathLength];
593 while (numArgs > 0) free(fileArgs[numArgs--]);
595 //Read the AppleEvent
596 err = AEGetParamDesc(theAppleEvent, keyDirectObject, typeAEList,
597 &fileSpecList);
599 err = AECountItems(&fileSpecList, &count); //Count number of files
601 for (i = 1; i <= count; i++) { //iteratively process each file
602 //get fsspec from apple event
603 if (! (err = AEGetNthPtr(&fileSpecList, i, typeFSS, &keyword, &type,
604 (Ptr)&fileSpec, sizeof(FSSpec), &actualSize)))
605 {
606 //get path from file spec
607 if ((err = FSMakePath(fileSpec, (unsigned char *)&path,
608 kMaxPathLength))) return err;
610 if (numArgs == kMaxArgumentsToScript) break;
612 if (! (fileArgs[numArgs] = malloc(kMaxPathLength))) return true;
614 strcpy(fileArgs[numArgs++], (char *)&path);
615 }
616 else return err;
617 }
619 if (! taskDone) pthread_create(&odtid, NULL, OpenDoc, NULL);
620 else pthread_create(&tid, NULL, Execute, NULL);
622 return err;
623 }
625 ///////////////////////////////
626 // Handler for clicking on app icon
627 ///////////////////////////////
628 static OSErr AppOpenAppAEHandler(const AppleEvent *theAppleEvent,
629 AppleEvent *reply, long refCon)
630 {
631 #pragma unused (reply, refCon, theAppleEvent)
633 // the app has been opened without any items dragged on to it
634 pthread_create(&tid, NULL, Execute, NULL);
636 return noErr;
637 }
640 static void OpenURL(Str255 url)
641 {
642 // Use Internet Config to hand the URL to the appropriate application, as
643 // set by the user in the Internet Preferences pane.
644 ICInstance icInstance;
645 // Applications creator code:
646 OSType signature = 'Inks';
647 OSStatus error = ICStart( &icInstance, signature );
648 if ( error == noErr )
649 {
650 ConstStr255Param hint = 0x0;
651 const char* data = url;
652 long length = strlen(url);
653 long start = 0;
654 long end = length;
655 // Don't bother testing return value (error); launched application will
656 // report problems.
657 ICLaunchURL( icInstance, hint, data, length, &start, &end );
658 ICStop( icInstance );
659 }
660 }
663 //////////////////////////////////
664 // Handler for when X11 fails to start
665 //////////////////////////////////
666 static OSStatus X11FailedHandler(EventHandlerCallRef theHandlerCall,
667 EventRef theEvent, void *userData)
668 {
669 #pragma unused(theHanderCall, theEvent, userData)
671 pthread_join(tid, NULL);
672 if (odtid) pthread_join(odtid, NULL);
674 SInt16 itemHit;
675 const char *getX11 = "\pGet X11 for Panther";
677 AlertStdAlertParamRec params;
678 params.movable = true;
679 params.helpButton = false;
680 params.filterProc = NULL;
681 params.defaultText = (StringPtr) kAlertDefaultOKText;
682 params.cancelText = getX11;
683 params.otherText = NULL;
684 params.defaultButton = kAlertStdAlertOKButton;
685 params.cancelButton = kAlertStdAlertCancelButton;
686 params.position = kWindowDefaultPosition;
688 StandardAlert(kAlertStopAlert, "\pFailed to start X11",
689 "\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.",
690 ¶ms, &itemHit);
692 if (itemHit == kAlertStdAlertCancelButton)
693 {
694 OpenURL("http://www.apple.com/downloads/macosx/apple/x11formacosx.html");
695 }
697 ExitToShell();
700 return noErr;
701 }