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>
41 ///////////////////////////////////////
42 // Definitions
43 ///////////////////////////////////////
44 #pragma mark Definitions
46 // name length limits
47 #define kMaxPathLength 1024
49 // names of files bundled with app
50 #define kScriptFileName "script"
51 #define kOpenDocFileName "openDoc"
53 // custom carbon events
54 #define kEventClassRedFatalAlert 911
55 #define kEventKindX11Failed 911
56 #define kEventKindFCCacheFailed 912
58 //maximum arguments the script accepts
59 #define kMaxArgumentsToScript 252
61 ///////////////////////////////////////
62 // Prototypes
63 ///////////////////////////////////////
64 #pragma mark Prototypes
66 static void *Execute(void *arg);
67 static void *OpenDoc(void *arg);
68 static OSErr ExecuteScript(char *script, pid_t *pid);
70 static void GetParameters(void);
71 static char* GetScript(void);
72 static char* GetOpenDoc(void);
74 OSErr LoadMenuBar(char *appName);
76 static OSStatus FSMakePath(FSSpec file, char *path, long maxPathSize);
77 static void RedFatalAlert(Str255 errorString, Str255 expStr);
78 static short DoesFileExist(char *path);
79 static OSStatus FixFCCache(void);
81 static OSErr AppQuitAEHandler(const AppleEvent *theAppleEvent,
82 AppleEvent *reply, long refCon);
83 static OSErr AppOpenDocAEHandler(const AppleEvent *theAppleEvent,
84 AppleEvent *reply, long refCon);
85 static OSErr AppOpenAppAEHandler(const AppleEvent *theAppleEvent,
86 AppleEvent *reply, long refCon);
87 static OSStatus X11FailedHandler(EventHandlerCallRef theHandlerCall,
88 EventRef theEvent, void *userData);
89 static OSStatus FCCacheFailedHandler(EventHandlerCallRef theHandlerCall,
90 EventRef theEvent, void *userData);
91 ///////////////////////////////////////
92 // Globals
93 ///////////////////////////////////////
94 #pragma mark Globals
96 // process id of forked process
97 pid_t pid = 0;
99 // thread id of threads that start scripts
100 pthread_t odtid = 0, tid = 0;
102 // indicator of whether the script has completed executing
103 short taskDone = true;
105 // execution parameters
106 char scriptPath[kMaxPathLength];
107 char openDocPath[kMaxPathLength];
109 //arguments to the script
110 char *arguments[kMaxArgumentsToScript+3];
111 char *fileArgs[kMaxArgumentsToScript];
112 short numArgs = 0;
114 extern char **environ;
116 #pragma mark -
118 ///////////////////////////////////////
119 // Program entrance point
120 ///////////////////////////////////////
121 int main(int argc, char* argv[])
122 {
123 OSErr err = noErr;
124 EventTypeSpec X11events = { kEventClassRedFatalAlert, kEventKindX11Failed };
125 EventTypeSpec FCCacheEvents = { kEventClassRedFatalAlert, kEventKindFCCacheFailed };
127 InitCursor();
129 //install Apple Event handlers
130 err += AEInstallEventHandler(kCoreEventClass, kAEQuitApplication,
131 NewAEEventHandlerUPP(AppQuitAEHandler),
132 0, false);
133 err += AEInstallEventHandler(kCoreEventClass, kAEOpenDocuments,
134 NewAEEventHandlerUPP(AppOpenDocAEHandler),
135 0, false);
136 err += AEInstallEventHandler(kCoreEventClass, kAEOpenApplication,
137 NewAEEventHandlerUPP(AppOpenAppAEHandler),
138 0, false);
139 err += InstallEventHandler(GetApplicationEventTarget(),
140 NewEventHandlerUPP(X11FailedHandler), 1,
141 &X11events, NULL, NULL);
142 err += InstallEventHandler(GetApplicationEventTarget(),
143 NewEventHandlerUPP(FCCacheFailedHandler), 1,
144 &FCCacheEvents, NULL, NULL);
146 if (err) RedFatalAlert("\pInitialization Error",
147 "\pError initing Apple Event handlers.");
149 //create the menu bar
150 if (err = LoadMenuBar(NULL)) RedFatalAlert("\pInitialization Error",
151 "\pError loading MenuBar.nib.");
153 GetParameters(); //load data from files containing exec settings
155 RunApplicationEventLoop(); //Run the event loop
156 return 0;
157 }
159 #pragma mark -
162 //////////////////////////////////
163 // Handler for when X11 fails to start
164 //////////////////////////////////
165 static OSStatus FCCacheFailedHandler(EventHandlerCallRef theHandlerCall,
166 EventRef theEvent, void *userData)
167 {
169 pthread_join(tid, NULL);
170 if (odtid) pthread_join(odtid, NULL);
172 SInt16 itemHit;
174 AlertStdAlertParamRec params;
175 params.movable = true;
176 params.helpButton = false;
177 params.filterProc = NULL;
178 params.defaultText = "\pRun fc-cache";
179 params.cancelText = "\pIgnore";
180 params.otherText = NULL;
181 params.defaultButton = kAlertStdAlertOKButton;
182 params.cancelButton = kAlertStdAlertCancelButton;
183 params.position = kWindowDefaultPosition;
185 StandardAlert(kAlertStopAlert, "\pFont caches may need to be updated",
186 "\pThere is a problem on OS X 10.4.x where X11 installation does not always generate the necessary fontconfig caches. This can be corrected by running /usr/X11R6/bin/fc-cache as root.\n\nThis may take some time. Please do not close Inkscape.",
187 ¶ms, &itemHit);
189 if (itemHit == kAlertStdAlertOKButton)
190 {
191 OSStatus err = FixFCCache();
193 if (err == errAuthorizationSuccess)
194 {
195 params.defaultText = (void *) kAlertDefaultOKText;
196 params.cancelText = NULL;
198 StandardAlert(kAlertNoteAlert, "\pFont caches have been updated",
199 "\pPlease re-run Inkscape.", ¶ms, &itemHit);
200 system("test -d $HOME/.inkscape || mkdir $HOME/.inkscape; touch $HOME/.inkscape/.fccache");
201 }
202 }
203 else
204 {
205 params.defaultText = (void *) kAlertDefaultOKText;
206 params.cancelText = NULL;
208 StandardAlert(kAlertNoteAlert, "\pFont caches have not been updated",
209 "\pThey can be updated manually by running the following:\n sudo /usr/X11R6/bin/fc-cache\nOnce you have dealt with this, please re-run Inkscape.", ¶ms, &itemHit);
210 system("test -d $HOME/.inkscape || mkdir $HOME/.inkscape; touch $HOME/.inkscape/.fccache");
211 }
213 ExitToShell();
215 return noErr;
216 }
219 /////////////////////////////////////
220 // Code to run fc-cache on first run
221 /////////////////////////////////////
222 static OSStatus FixFCCache (void)
223 {
224 char* args[1];
226 // Run fc-cache
227 AuthorizationItem authItems[] =
228 {
229 {
230 kAuthorizationRightExecute,
231 23,
232 "/usr/X11R6/bin/fc-cache",
233 0
234 }
235 };
236 AuthorizationItemSet authItemSet =
237 {
238 1,
239 authItems
240 };
241 AuthorizationRef authRef = NULL;
242 OSStatus err = AuthorizationCreate (NULL, &authItemSet,
243 kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authRef);
245 if (err == errAuthorizationSuccess)
246 {
247 //the arguments parameter to AuthorizationExecuteWithPrivileges is
248 //a NULL terminated array of C string pointers.
249 args[0]= NULL;
251 AuthorizationExecuteWithPrivileges (authRef, "/usr/X11R6/bin/fc-cache",
252 kAuthorizationFlagDefaults, args, NULL);
253 }
254 AuthorizationFree(authRef, kAuthorizationFlagDestroyRights);
256 return err;
257 }
259 ///////////////////////////////////
260 // Execution thread starts here
261 ///////////////////////////////////
262 static void *Execute (void *arg)
263 {
264 EventRef event;
266 taskDone = false;
268 OSErr err = ExecuteScript(scriptPath, &pid);
269 if (err == (OSErr)11) {
270 CreateEvent(NULL, kEventClassRedFatalAlert, kEventKindX11Failed, 0,
271 kEventAttributeNone, &event);
272 PostEventToQueue(GetMainEventQueue(), event, kEventPriorityStandard);
273 }
274 else if (err == (OSErr)12) {
275 CreateEvent(NULL, kEventClassRedFatalAlert, kEventKindFCCacheFailed, 0,
276 kEventAttributeNone, &event);
277 PostEventToQueue(GetMainEventQueue(), event, kEventPriorityStandard);
278 }
279 else ExitToShell();
280 return 0;
281 }
283 ///////////////////////////////////
284 // Open additional documents thread starts here
285 ///////////////////////////////////
286 static void *OpenDoc (void *arg)
287 {
288 ExecuteScript(openDocPath, NULL);
289 return 0;
290 }
292 ///////////////////////////////////////
293 // Run a script via the system command
294 ///////////////////////////////////////
295 static OSErr ExecuteScript (char *script, pid_t *pid)
296 {
297 pid_t wpid = 0, p = 0;
298 int status, i;
300 if (! pid) pid = &p;
302 // Generate the array of argument strings before we do any executing
303 arguments[0] = script;
304 for (i = 0; i < numArgs; i++) arguments[i + 1] = fileArgs[i];
305 arguments[i + 1] = NULL;
307 *pid = fork(); //open fork
309 if (*pid == (pid_t)-1) exit(13); //error
310 else if (*pid == 0) { //child process started
311 execve(arguments[0], arguments, environ);
312 exit(13); //if we reach this point, there's an error
313 }
315 wpid = waitpid(*pid, &status, 0); //wait while child process finishes
317 if (wpid == (pid_t)-1) return wpid;
318 return (OSErr)WEXITSTATUS(status);
319 }
321 #pragma mark -
323 ///////////////////////////////////////
324 // This function loads all the neccesary settings
325 // from config files in the Resources folder
326 ///////////////////////////////////////
327 static void GetParameters (void)
328 {
329 char *str;
330 if (! (str = (char *)GetScript())) //get path to script to be executed
331 RedFatalAlert("\pInitialization Error",
332 "\pError getting script from application bundle.");
333 strcpy((char *)&scriptPath, str);
335 if (! (str = (char *)GetOpenDoc())) //get path to openDoc
336 RedFatalAlert("\pInitialization Error",
337 "\pError getting openDoc from application bundle.");
338 strcpy((char *)&openDocPath, str);
339 }
341 ///////////////////////////////////////
342 // Get path to the script in Resources folder
343 ///////////////////////////////////////
344 static char* GetScript (void)
345 {
346 CFStringRef fileName;
347 CFBundleRef appBundle;
348 CFURLRef scriptFileURL;
349 FSRef fileRef;
350 FSSpec fileSpec;
351 char *path;
353 //get CF URL for script
354 if (! (appBundle = CFBundleGetMainBundle())) return NULL;
355 if (! (fileName = CFStringCreateWithCString(NULL, kScriptFileName,
356 kCFStringEncodingASCII)))
357 return NULL;
358 if (! (scriptFileURL = CFBundleCopyResourceURL(appBundle, fileName, NULL,
359 NULL))) return NULL;
361 //Get file reference from Core Foundation URL
362 if (! CFURLGetFSRef(scriptFileURL, &fileRef)) return NULL;
364 //dispose of the CF variables
365 CFRelease(scriptFileURL);
366 CFRelease(fileName);
368 //convert FSRef to FSSpec
369 if (FSGetCatalogInfo(&fileRef, kFSCatInfoNone, NULL, NULL, &fileSpec,
370 NULL)) return NULL;
372 //create path string
373 if (! (path = malloc(kMaxPathLength))) return NULL;
374 if (FSMakePath(fileSpec, path, kMaxPathLength)) return NULL;
375 if (! DoesFileExist(path)) return NULL;
377 return path;
378 }
380 ///////////////////////////////////////
381 // Gets the path to openDoc in Resources folder
382 ///////////////////////////////////////
383 static char* GetOpenDoc (void)
384 {
385 CFStringRef fileName;
386 CFBundleRef appBundle;
387 CFURLRef openDocFileURL;
388 FSRef fileRef;
389 FSSpec fileSpec;
390 char *path;
392 //get CF URL for openDoc
393 if (! (appBundle = CFBundleGetMainBundle())) return NULL;
394 if (! (fileName = CFStringCreateWithCString(NULL, kOpenDocFileName,
395 kCFStringEncodingASCII)))
396 return NULL;
397 if (! (openDocFileURL = CFBundleCopyResourceURL(appBundle, fileName, NULL,
398 NULL))) return NULL;
400 //Get file reference from Core Foundation URL
401 if (! CFURLGetFSRef( openDocFileURL, &fileRef )) return NULL;
403 //dispose of the CF variables
404 CFRelease(openDocFileURL);
405 CFRelease(fileName);
407 //convert FSRef to FSSpec
408 if (FSGetCatalogInfo(&fileRef, kFSCatInfoNone, NULL, NULL, &fileSpec,
409 NULL)) return NULL;
411 //create path string
412 if (! (path = malloc(kMaxPathLength))) return NULL;
413 if (FSMakePath(fileSpec, path, kMaxPathLength)) return NULL;
414 if (! DoesFileExist(path)) return NULL;
416 return path;
417 }
419 #pragma mark -
421 /////////////////////////////////////
422 // Load menu bar from nib
423 /////////////////////////////////////
424 OSErr LoadMenuBar (char *appName)
425 {
426 OSErr err;
427 IBNibRef nibRef;
429 if (err = CreateNibReference(CFSTR("MenuBar"), &nibRef)) return err;
430 if (err = SetMenuBarFromNib(nibRef, CFSTR("MenuBar"))) return err;
431 DisposeNibReference(nibRef);
433 return noErr;
434 }
436 #pragma mark -
438 ///////////////////////////////////////
439 // Generate path string from FSSpec record
440 ///////////////////////////////////////
441 static OSStatus FSMakePath(FSSpec file, char *path, long maxPathSize)
442 {
443 OSErr err = noErr;
444 FSRef fileRef;
446 //create file reference from file spec
447 if (err = FSpMakeFSRef(&file, &fileRef)) return err;
449 // and then convert the FSRef to a path
450 return FSRefMakePath(&fileRef, path, maxPathSize);
451 }
453 ////////////////////////////////////////
454 // Standard red error alert, then exit application
455 ////////////////////////////////////////
456 static void RedFatalAlert (Str255 errorString, Str255 expStr)
457 {
458 StandardAlert(kAlertStopAlert, errorString, expStr, NULL, NULL);
459 ExitToShell();
460 }
462 ///////////////////////////////////////
463 // Determines whether file exists at path or not
464 ///////////////////////////////////////
465 static short DoesFileExist (char *path)
466 {
467 if (access(path, F_OK) == -1) return false;
468 return true;
469 }
471 #pragma mark -
473 ///////////////////////////////////////
474 // Apple Event handler for Quit i.e. from
475 // the dock or Application menu item
476 ///////////////////////////////////////
477 static OSErr AppQuitAEHandler(const AppleEvent *theAppleEvent,
478 AppleEvent *reply, long refCon)
479 {
480 #pragma unused (reply, refCon, theAppleEvent)
482 while (numArgs > 0) free(fileArgs[numArgs--]);
484 if (! taskDone && pid) { //kill the script process brutally
485 kill(pid, 9);
486 printf("Platypus App: PID %d killed brutally\n", pid);
487 }
489 pthread_cancel(tid);
490 if (odtid) pthread_cancel(odtid);
492 ExitToShell();
494 return noErr;
495 }
497 /////////////////////////////////////
498 // Handler for docs dragged on app icon
499 /////////////////////////////////////
500 static OSErr AppOpenDocAEHandler(const AppleEvent *theAppleEvent,
501 AppleEvent *reply, long refCon)
502 {
503 #pragma unused (reply, refCon)
505 OSErr err = noErr;
506 AEDescList fileSpecList;
507 AEKeyword keyword;
508 DescType type;
510 short i;
511 long count, actualSize;
513 FSSpec fileSpec;
514 char path[kMaxPathLength];
516 while (numArgs > 0) free(fileArgs[numArgs--]);
518 //Read the AppleEvent
519 err = AEGetParamDesc(theAppleEvent, keyDirectObject, typeAEList,
520 &fileSpecList);
522 err = AECountItems(&fileSpecList, &count); //Count number of files
524 for (i = 1; i <= count; i++) { //iteratively process each file
525 //get fsspec from apple event
526 if (! (err = AEGetNthPtr(&fileSpecList, i, typeFSS, &keyword, &type,
527 (Ptr)&fileSpec, sizeof(FSSpec), &actualSize)))
528 {
529 //get path from file spec
530 if ((err = FSMakePath(fileSpec, (unsigned char *)&path,
531 kMaxPathLength))) return err;
533 if (numArgs == kMaxArgumentsToScript) break;
535 if (! (fileArgs[numArgs] = malloc(kMaxPathLength))) return true;
537 strcpy(fileArgs[numArgs++], (char *)&path);
538 }
539 else return err;
540 }
542 if (! taskDone) pthread_create(&odtid, NULL, OpenDoc, NULL);
543 else pthread_create(&tid, NULL, Execute, NULL);
545 return err;
546 }
548 ///////////////////////////////
549 // Handler for clicking on app icon
550 ///////////////////////////////
551 static OSErr AppOpenAppAEHandler(const AppleEvent *theAppleEvent,
552 AppleEvent *reply, long refCon)
553 {
554 #pragma unused (reply, refCon, theAppleEvent)
556 // the app has been opened without any items dragged on to it
557 pthread_create(&tid, NULL, Execute, NULL);
559 return noErr;
560 }
563 static void OpenURL(Str255 url)
564 {
565 // Use Internet Config to hand the URL to the appropriate application, as
566 // set by the user in the Internet Preferences pane.
567 ICInstance icInstance;
568 // Applications creator code:
569 OSType signature = 'Inks';
570 OSStatus error = ICStart( &icInstance, signature );
571 if ( error == noErr )
572 {
573 ConstStr255Param hint = 0x0;
574 const char* data = url;
575 long length = strlen(url);
576 long start = 0;
577 long end = length;
578 // Don't bother testing return value (error); launched application will
579 // report problems.
580 ICLaunchURL( icInstance, hint, data, length, &start, &end );
581 ICStop( icInstance );
582 }
583 }
586 //////////////////////////////////
587 // Handler for when X11 fails to start
588 //////////////////////////////////
589 static OSStatus X11FailedHandler(EventHandlerCallRef theHandlerCall,
590 EventRef theEvent, void *userData)
591 {
592 #pragma unused(theHanderCall, theEvent, userData)
594 pthread_join(tid, NULL);
595 if (odtid) pthread_join(odtid, NULL);
597 SInt16 itemHit;
598 const char *getX11 = "\pGet X11 for Panther";
600 AlertStdAlertParamRec params;
601 params.movable = true;
602 params.helpButton = false;
603 params.filterProc = NULL;
604 params.defaultText = (StringPtr) kAlertDefaultOKText;
605 params.cancelText = getX11;
606 params.otherText = NULL;
607 params.defaultButton = kAlertStdAlertOKButton;
608 params.cancelButton = kAlertStdAlertCancelButton;
609 params.position = kWindowDefaultPosition;
611 StandardAlert(kAlertStopAlert, "\pFailed to start X11",
612 "\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.",
613 ¶ms, &itemHit);
615 if (itemHit == kAlertStdAlertCancelButton)
616 {
617 OpenURL("http://www.apple.com/downloads/macosx/apple/x11formacosx.html");
618 }
620 ExitToShell();
623 return noErr;
624 }