source: trunk/appswitch/appswitch/main.c @ 163

Last change on this file since 163 was 163, checked in by Nicholas Riley, 16 years ago

main.c: fix the copyright statement

File size: 18.5 KB
Line 
1/*
2 appswitch - a command-line application switcher
3 Nicholas Riley <appswitch@sabi.net>
4
5 Copyright (c) 2003-04, Nicholas Riley
6 All rights reserved.
7
8 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
9
10 * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
11 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
12 * Neither the name of this software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
13
14 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.
15
16*/
17
18#define DEBUG 0
19
20#include <unistd.h>
21#include <sys/ioctl.h>
22#include "CPS.h"
23
24const char *APP_NAME;
25
26#define VERSION "1.0.1"
27
28struct {
29    OSType creator;
30    CFStringRef bundleID;
31    char *name;
32    pid_t pid;
33    char *path;
34    enum {
35        MATCH_UNKNOWN, MATCH_FRONT, MATCH_CREATOR, MATCH_BUNDLE_ID, MATCH_NAME, MATCH_PID, MATCH_PATH, MATCH_ALL
36    } matchType;
37    enum {
38        APP_NONE, APP_SWITCH, APP_SHOW, APP_HIDE, APP_QUIT, APP_KILL, APP_KILL_HARD, APP_LIST, APP_PRINT_PID
39    } appAction;
40    Boolean longList;
41    enum {
42        ACTION_NONE, ACTION_SHOW_ALL, ACTION_HIDE_OTHERS
43    } action;
44    enum {
45        FINAL_NONE, FINAL_SWITCH
46    } finalAction;
47} OPTS =
48{
49    kLSUnknownCreator, NULL, NULL, -1, NULL, MATCH_UNKNOWN, APP_NONE, ACTION_NONE, FINAL_NONE, false
50};
51
52typedef struct {
53    OSStatus status;
54    const char *desc;
55} errRec, errList[];
56
57static errList ERRS = {
58    // Process Manager errors
59    { appIsDaemon, "application is background-only\n", },
60    { procNotFound, "unable to connect to system service.\nAre you logged in?" },
61    // CoreGraphics errors
62    { kCGErrorIllegalArgument, "window server error.\nAre you logged in?" },
63    { fnfErr, "file not found" },
64    { 0, NULL }
65};
66
67void usage() {
68    fprintf(stderr, "usage: %s [-sShHqkFlLP] [-c creator] [-i bundleID] [-a name] [-p pid] [path]\n"
69            "  -s            show application, bring windows to front (do not switch)\n"
70            "  -S            show all applications\n"
71            "  -h            hide application\n"
72            "  -H            hide other applications\n"
73            "  -q            quit application\n"
74            "  -k            kill application (SIGINT)\n"
75            "  -K            kill application hard (SIGKILL)\n"
76            "  -l            list applications\n"
77            "  -L            list applications including full paths and bundle identifiers\n"
78            "  -P            print application process ID\n"
79            "  -F            bring current application's windows to front\n"
80            "  -c creator    match application by four-character creator code ('ToyS')\n"
81            "  -i bundle ID  match application by bundle identifier (com.apple.scripteditor)\n"
82            "  -p pid        match application by process identifier [slower]\n"
83            "  -a name       match application by name\n"
84            , APP_NAME);
85    fprintf(stderr, "appswitch "VERSION" (c) 2003-04 Nicholas Riley <http://web.sabi.net/nriley/software/>.\n"
86            "Please send bugs, suggestions, etc. to <appswitch@sabi.net>.\n");
87
88    exit(1);
89}
90
91char *osstatusstr(OSStatus err) {
92    errRec *rec;
93    const char *errDesc = "unknown error";
94    char * const failedStr = "(unable to retrieve error message)";
95    static char *str = NULL;
96    size_t len;
97    if (str != NULL && str != failedStr) free(str);
98    for (rec = &(ERRS[0]) ; rec->status != 0 ; rec++)
99        if (rec->status == err) {
100            errDesc = rec->desc;
101            break;
102        }
103            len = strlen(errDesc) + 10 * sizeof(char);
104    str = (char *)malloc(len);
105    if (str != NULL)
106        snprintf(str, len, "%s (%ld)", errDesc, err);
107    else
108        str = failedStr;
109    return str;
110}
111
112void osstatusexit(OSStatus err, const char *fmt, ...) {
113    va_list ap;
114    const char *errDesc = osstatusstr(err);
115    va_start(ap, fmt);
116    fprintf(stderr, "%s: ", APP_NAME);
117    vfprintf(stderr, fmt, ap);
118    fprintf(stderr, ": %s\n", errDesc);
119    exit(1);
120}
121
122void errexit(const char *fmt, ...) {
123    va_list ap;
124    va_start(ap, fmt);
125    fprintf(stderr, "%s: ", APP_NAME);
126    vfprintf(stderr, fmt, ap);
127    fprintf(stderr, "\n");
128    exit(1);
129}
130
131void getargs(int argc, char * const argv[]) {
132    extern char *optarg;
133    extern int optind;
134    int ch;
135
136    if (argc == 1) usage();
137
138    const char *opts = "c:i:p:a:sShHqkKlLPF";
139
140    while ( (ch = getopt(argc, argv, opts)) != -1) {
141        switch (ch) {
142            case 'p':
143                if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
144                if (sscanf(optarg, "%d", &OPTS.pid) != 1 || OPTS.pid < 0)
145                    errexit("invalid process identifier (argument of -p)");
146                OPTS.matchType = MATCH_PID;
147                break;
148            case 'c':
149                if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
150                if (strlen(optarg) != 4) errexit("creator (argument of -c) must be four characters long");
151                OPTS.creator = *(OSTypePtr)optarg;
152                OPTS.matchType = MATCH_CREATOR;
153                break;
154            case 'i':
155                if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
156                OPTS.bundleID = CFStringCreateWithCString(NULL, optarg, CFStringGetSystemEncoding());
157                OPTS.matchType = MATCH_BUNDLE_ID;
158                break;
159            case 'a':
160                if (OPTS.matchType != MATCH_UNKNOWN) errexit("choose only one of -c, -i, -p, -a options");
161                OPTS.name = strdup(optarg);
162                OPTS.matchType = MATCH_NAME;
163                break;
164            case 's':
165                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
166                OPTS.appAction = APP_SHOW;
167                break;
168            case 'h':
169                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
170                OPTS.appAction = APP_HIDE;
171                break;
172            case 'q':
173                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
174                OPTS.appAction = APP_QUIT;
175                break;
176            case 'k':
177                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
178                OPTS.appAction = APP_KILL;
179                break;
180            case 'K':
181                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
182                OPTS.appAction = APP_KILL_HARD;
183                break;
184            case 'l':
185                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
186                OPTS.appAction = APP_LIST;
187                break;
188            case 'L':
189                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -L, -P options");
190                OPTS.appAction = APP_LIST;
191                OPTS.longList = true;
192                break;
193            case 'P':
194                if (OPTS.appAction != APP_NONE) errexit("choose only one of -s, -h, -q, -k, -K, -l, -P options");
195                OPTS.appAction = APP_PRINT_PID;
196                break;
197            case 'S':
198                if (OPTS.action != ACTION_NONE) errexit("choose -S, -H or neither option");
199                OPTS.action = ACTION_SHOW_ALL;
200                break;
201            case 'H':
202                if (OPTS.action != ACTION_NONE) errexit("choose -S, -H or neither option");
203                OPTS.action = ACTION_HIDE_OTHERS;
204                break;
205            case 'F':
206                if (OPTS.finalAction != FINAL_NONE) errexit("choose only one -F option");
207                OPTS.finalAction = FINAL_SWITCH;
208                break;
209            default: usage();
210        }
211    }
212
213    argc -= optind;
214    argv += optind;
215
216    if (OPTS.matchType != MATCH_UNKNOWN && argc != 0) usage();
217
218    if (OPTS.matchType == MATCH_UNKNOWN) {
219        if (argc == 0) {
220            if (OPTS.appAction == APP_LIST) {
221                OPTS.matchType = MATCH_ALL;
222            } else if (OPTS.action != ACTION_NONE || OPTS.finalAction != FINAL_NONE) {
223                OPTS.matchType = MATCH_FRONT;
224            } else usage();
225        } else if (argc == 1) {
226            OPTS.path = argv[0];
227            OPTS.matchType = MATCH_PATH;
228        } else usage();
229    }
230
231    if (OPTS.matchType != MATCH_FRONT && OPTS.appAction == APP_NONE)
232        OPTS.appAction = APP_SWITCH;
233
234}
235
236CPSProcessSerNum frontApplication() {
237    CPSProcessSerNum psn;
238    OSStatus err = CPSGetFrontProcess(&psn);
239    if (err != noErr) osstatusexit(err, "can't get frontmost process");
240#if DEBUG
241    fprintf(stderr, "front application PSN %ld.%ld\n", psn.hi, psn.lo);
242#endif
243    return psn;
244}
245
246Boolean bundleIdentifierForApplication(CFStringRef *bundleID, char *path) {
247    CFURLRef url = CFURLCreateFromFileSystemRepresentation(NULL, path, strlen(path), false);
248    if (url == NULL) return false;
249    CFBundleRef bundle = CFBundleCreate(NULL, url);
250    if (bundle != NULL) {
251        *bundleID = CFBundleGetIdentifier(bundle);
252        if (*bundleID != NULL) {
253            CFRetain(*bundleID);
254#if DEBUG
255            CFShow(*bundleID);
256#endif
257        }
258        CFRelease(bundle);
259    } else {
260        *bundleID = NULL;
261    }
262    CFRelease(url);
263    return true;
264}
265
266OSStatus quitApplication(CPSProcessSerNum *psn) {
267    AppleEvent event;
268    AEAddressDesc appDesc;
269    OSStatus err;
270
271    AEInitializeDesc(&appDesc);
272    err = AECreateDesc(typeProcessSerialNumber, psn, sizeof(*psn), &appDesc);
273    if (err != noErr) return err;
274
275    // XXX AECreateAppleEvent is very slow in Mac OS X 10.2.4 and earlier.
276    // XXX This is Apple's bug: <http://lists.apple.com/archives/applescript-implementors/2003/Feb/19/aecreateappleeventfromco.txt>
277    err = AECreateAppleEvent(kCoreEventClass, kAEQuitApplication, &appDesc, kAutoGenerateReturnID, kAnyTransactionID, &event);
278    if (err != noErr) return err;
279
280    AppleEvent nullReply = {typeNull, nil};
281    err = AESendMessage(&event, &nullReply, kAENoReply, kNoTimeOut);
282    (void)AEDisposeDesc(&event);
283    if (err != noErr) return err;
284
285    (void)AEDisposeDesc(&nullReply); // according to docs, don't call unless AESend returned successfully
286
287    return noErr;
288}
289
290CPSProcessSerNum matchApplication(CPSProcessInfoRec *info) {
291    long pathMaxLength = pathconf("/", _PC_PATH_MAX);
292    long nameMaxLength = pathconf("/", _PC_NAME_MAX);
293
294    char *path = (char *)malloc(pathMaxLength);
295    char *name = (char *)malloc(nameMaxLength);;
296
297    if (path == NULL || name == NULL) errexit("can't allocate memory for path or filename buffer");
298
299    if (OPTS.matchType == MATCH_FRONT) return frontApplication();
300
301    OSStatus err;
302    CPSProcessSerNum psn = {
303        kNoProcess, kNoProcess
304    };
305    int len;
306    char *format = NULL;
307    if (OPTS.appAction == APP_LIST) {
308        int termwidth = 80;
309        struct winsize ws;
310        char *banner = "        PSN   PID TYPE CREA NAME               ";
311                     // 123456789.0 12345 1234 1234 1234567890123456789
312        if ((ioctl(STDOUT_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
313             ioctl(STDERR_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
314             ioctl(STDIN_FILENO,  TIOCGWINSZ, (char *)&ws) != -1) ||
315            ws.ws_col != 0) termwidth = ws.ws_col;
316        char *formatButPath = "%9ld.%ld %5ld %c%c%c%c %c%c%c%c %-19.19s";
317        int pathlen = termwidth - strlen(banner) - 1;
318        // XXX don't ever free 'format', should fix if we get called repeatedly
319        if (OPTS.longList) {
320            printf("%s PATH (bundle identifier)\n", banner);
321            asprintf(&format, "%s %%s", formatButPath);
322        } else if (pathlen >= 4) {
323            printf("%s PATH\n", banner);
324            asprintf(&format, "%s %%-%d.%ds", formatButPath, pathlen, pathlen);
325        } else {
326            format = formatButPath;
327        }
328    }
329   
330    while ( (err = CPSGetNextProcess(&psn)) == noErr) {
331        err = CPSGetProcessInfo(&psn, info, path, pathMaxLength, &len, name, nameMaxLength);
332        if (err != noErr) osstatusexit(err, "can't get information for process PSN %ld.%ld", psn.hi, psn.lo);
333
334#if DEBUG
335        fprintf(stderr, "%ld.%ld: %s : %s\n", psn.hi, psn.lo, name, path);
336#endif
337
338        switch (OPTS.matchType) {
339            case MATCH_ALL:
340                break;
341            case MATCH_CREATOR: if (OPTS.creator != info->ExecFileCreator) continue;
342                break;
343            case MATCH_NAME: if (strcmp(name, OPTS.name) != 0) continue;
344                break;
345            case MATCH_PID: if (OPTS.pid != info->UnixPID) continue;
346                break;
347            case MATCH_PATH: if (strcmp(path, OPTS.path) != 0) continue;
348                break;
349            case MATCH_BUNDLE_ID:
350               {
351                   CFStringRef bundleID;
352                   if (!bundleIdentifierForApplication(&bundleID, path))
353                       errexit("can't get bundle location for process '%s' (PSN %ld.%ld, pid %ld)", name, psn.hi, psn.lo, info->UnixPID);
354                   if (bundleID != NULL) {
355                       CFComparisonResult result = CFStringCompare(OPTS.bundleID, bundleID, kCFCompareCaseInsensitive);
356                       if (result == kCFCompareEqualTo)
357                           break;
358                       CFRelease(bundleID);
359                   }
360                   continue;
361               }
362            default:
363                errexit("internal error: invalid match type");
364        }
365        if (OPTS.appAction == APP_LIST) {
366            char *type = (char *)&(info->ExecFileType), *crea = (char *)&(info->ExecFileCreator);
367#define CXX(c) ( (c) < ' ' ? ' ' : (c) )
368#define OSTYPE_CHAR_ARGS(t) CXX(t[0]), CXX(t[1]), CXX(t[2]), CXX(t[3])
369            printf(format, psn.hi, psn.lo, info->UnixPID,
370                   OSTYPE_CHAR_ARGS(type), OSTYPE_CHAR_ARGS(crea),
371                   name, path);
372            if (OPTS.longList) {
373                CFStringRef bundleID = NULL;
374                if (!bundleIdentifierForApplication(&bundleID, path))
375                    errexit("can't get bundle location for process '%s' (PSN %ld.%ld, pid %ld)", name, psn.hi, psn.lo, info->UnixPID);
376                if (bundleID != NULL) {
377                    char *bundleIDStr = (char *)CFStringGetCStringPtr(bundleID, CFStringGetSystemEncoding());
378                    if (bundleIDStr == NULL) {
379                        CFIndex bundleIDLength = CFStringGetLength(bundleID) + 1;
380                        bundleIDStr = (char *)malloc(bundleIDLength * sizeof(char));
381                        if (!CFStringGetCString(bundleID, bundleIDStr, bundleIDLength, CFStringGetSystemEncoding())) {
382                            CFShow(bundleIDStr);
383                            errexit("internal error: string encoding conversion failed for bundle identifier");
384                        }
385                        printf(" (%s)", bundleIDStr);
386                        free(bundleIDStr);
387                    } else {
388                        printf(" (%s)", bundleIDStr);
389                    }
390                    CFRelease(bundleID);
391                }
392            }
393            putchar('\n');
394            continue;
395        }
396        return psn;
397    }
398    if (err != procNotFound) osstatusexit(err, "can't get next process");
399
400    if (OPTS.appAction == APP_LIST) return frontApplication();
401
402    errexit("can't find matching process");
403    return psn;
404}
405
406int main(int argc, char * const argv[]) {
407    OSStatus err = noErr;
408
409    APP_NAME = argv[0];
410    getargs(argc, argv);
411
412    // need to establish connection with window server
413    InitCursor();
414
415    CPSProcessInfoRec info;
416    CPSProcessSerNum psn = matchApplication(&info);
417
418    const char *verb = NULL;
419    switch (OPTS.appAction) {
420        case APP_NONE: break;
421        case APP_LIST: break; // already handled in matchApplication
422        case APP_SWITCH: err = CPSSetFrontProcess(&psn); verb = "set front"; break;
423        case APP_SHOW: err = CPSPostShowReq(&psn); verb = "show"; break;
424        case APP_HIDE: err = CPSPostHideReq(&psn); verb = "hide"; break;
425        case APP_QUIT: err = quitApplication(&psn); verb = "quit"; break;
426        case APP_KILL: err = CPSPostKillRequest(&psn, kNilOptions); verb = "kill"; break;
427        case APP_KILL_HARD: err = CPSPostKillRequest(&psn, bfCPSKillHard); verb = "kill"; break;
428        case APP_PRINT_PID:
429            if (info.UnixPID <= 0) errexit("can't get process ID");
430            printf("%lu\n", info.UnixPID); // pid_t is signed, but this field isn't
431            break;
432        default:
433            errexit("internal error: invalid application action");
434    }
435    if (err != noErr) osstatusexit(err, "can't %s process", verb);
436
437    switch (OPTS.action) {
438        case ACTION_NONE: break;
439        case ACTION_SHOW_ALL: err = CPSPostShowAllReq(&psn); verb = "show all"; break;
440        case ACTION_HIDE_OTHERS: err = CPSPostHideMostReq(&psn); verb = "hide other"; break;
441        default:
442            errexit("internal error: invalid action");
443    }
444    if (err != noErr) osstatusexit(err, "can't %s processes", verb);
445
446    switch (OPTS.finalAction) {
447        case FINAL_NONE: break;
448        case FINAL_SWITCH:
449            psn = frontApplication();
450#if DEBUG
451            fprintf(stderr, "posting show request for %ld.%ld\n", psn.hi, psn.lo);
452#endif
453            if (OPTS.action != ACTION_NONE) usleep(750000); // XXX
454            err = CPSPostShowReq(&psn) || CPSSetFrontProcess(&psn);
455            verb = "bring current application's windows to the front";
456            break;
457        default:
458            errexit("internal error: invalid final action");   
459    }
460    if (err != noErr) osstatusexit(err, "can't %s", verb);
461
462    exit(0);
463}
Note: See TracBrowser for help on using the repository browser.