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

Last change on this file since 99 was 99, checked in by Nicholas Riley, 18 years ago

appswitch 1.0

File size: 18.2 KB
Line 
1/*
2 appswitch - a command-line application switcher
3 Nicholas Riley <appswitch@sabi.net>
4
5 Copyright (c) 2003, 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"
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 [-sShHkFlP] [-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 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 DEBUG
253        CFShow(*bundleID);
254#endif
255    }
256    CFRelease(url);
257    return true;
258}
259
260OSStatus quitApplication(CPSProcessSerNum *psn) {
261    AppleEvent event;
262    AEAddressDesc appDesc;
263    OSStatus err;
264
265    AEInitializeDesc(&appDesc);
266    err = AECreateDesc(typeProcessSerialNumber, psn, sizeof(*psn), &appDesc);
267    if (err != noErr) return err;
268
269    // XXX AECreateAppleEvent is very slow in Mac OS X 10.2.4 and earlier.
270    // XXX This is Apple's bug: <http://lists.apple.com/archives/applescript-implementors/2003/Feb/19/aecreateappleeventfromco.txt>
271    err = AECreateAppleEvent(kCoreEventClass, kAEQuitApplication, &appDesc, kAutoGenerateReturnID, kAnyTransactionID, &event);
272    if (err != noErr) return err;
273
274    AppleEvent nullReply = {typeNull, nil};
275    err = AESendMessage(&event, &nullReply, kAENoReply, kNoTimeOut);
276    (void)AEDisposeDesc(&event);
277    if (err != noErr) return err;
278
279    (void)AEDisposeDesc(&nullReply); // according to docs, don't call unless AESend returned successfully
280
281    return noErr;
282}
283
284CPSProcessSerNum matchApplication(CPSProcessInfoRec *info) {
285    long pathMaxLength = pathconf("/", _PC_PATH_MAX);
286    long nameMaxLength = pathconf("/", _PC_NAME_MAX);
287
288    char *path = (char *)malloc(pathMaxLength);
289    char *name = (char *)malloc(nameMaxLength);;
290
291    if (path == NULL || name == NULL) errexit("can't allocate memory for path or filename buffer");
292
293    if (OPTS.matchType == MATCH_FRONT) return frontApplication();
294
295    OSStatus err;
296    CPSProcessSerNum psn = {
297        kNoProcess, kNoProcess
298    };
299    int len;
300    char *format = NULL;
301    if (OPTS.appAction == APP_LIST) {
302        int termwidth = 80;
303        struct winsize ws;
304        char *banner = "       PSN   PID TYPE CREA NAME                ";
305                     // 12345678.0 12345 1234 1234 12345678901234567890
306        if ((ioctl(STDOUT_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
307             ioctl(STDERR_FILENO, TIOCGWINSZ, (char *)&ws) != -1 ||
308             ioctl(STDIN_FILENO,  TIOCGWINSZ, (char *)&ws) != -1) ||
309            ws.ws_col != 0) termwidth = ws.ws_col;
310        char *formatButPath = "%8ld.%ld %5ld %c%c%c%c %c%c%c%c %-20.20s";
311        int pathlen = termwidth - strlen(banner) - 1;
312        // XXX don't ever free 'format', should fix if we get called repeatedly
313        if (OPTS.longList) {
314            printf("%s PATH (bundle identifier)\n", banner);
315            asprintf(&format, "%s %%s", formatButPath);
316        } else if (pathlen >= 4) {
317            printf("%s PATH\n", banner);
318            asprintf(&format, "%s %%-%d.%ds", formatButPath, pathlen, pathlen);
319        } else {
320            format = formatButPath;
321        }
322    }
323   
324    while ( (err = CPSGetNextProcess(&psn)) == noErr) {
325        err = CPSGetProcessInfo(&psn, info, path, pathMaxLength, &len, name, nameMaxLength);
326        if (err != noErr) osstatusexit(err, "can't get information for process PSN %ld.%ld", psn.hi, psn.lo);
327
328#if DEBUG
329        fprintf(stderr, "%ld.%ld: %s : %s\n", psn.hi, psn.lo, name, path);
330#endif
331
332        switch (OPTS.matchType) {
333            case MATCH_ALL:
334                break;
335            case MATCH_CREATOR: if (OPTS.creator != info->ExecFileCreator) continue;
336                break;
337            case MATCH_NAME: if (strcmp(name, OPTS.name) != 0) continue;
338                break;
339            case MATCH_PID: if (OPTS.pid != info->UnixPID) continue;
340                break;
341            case MATCH_PATH: if (strcmp(path, OPTS.path) != 0) continue;
342                break;
343            case MATCH_BUNDLE_ID:
344               {
345                   CFStringRef bundleID;
346                   if (!bundleIdentifierForApplication(&bundleID, path))
347                       errexit("can't get bundle location for process '%s' (PSN %ld.%ld, pid %ld)", name, psn.hi, psn.lo, info->UnixPID);
348                   if (bundleID != NULL && CFStringCompare(OPTS.bundleID, bundleID, kCFCompareCaseInsensitive) == kCFCompareEqualTo)
349                       break;
350                   continue;
351               }
352            default:
353                errexit("internal error: invalid match type");
354        }
355        if (OPTS.appAction == APP_LIST) {
356            char *type = (char *)&(info->ExecFileType), *crea = (char *)&(info->ExecFileCreator);
357#define CXX(c) ( (c) < ' ' ? ' ' : (c) )
358#define OSTYPE_CHAR_ARGS(t) CXX(t[0]), CXX(t[1]), CXX(t[2]), CXX(t[3])
359            printf(format, psn.hi, psn.lo, info->UnixPID,
360                   OSTYPE_CHAR_ARGS(type), OSTYPE_CHAR_ARGS(crea),
361                   name, path);
362            if (OPTS.longList) {
363                CFStringRef bundleID = NULL;
364                if (!bundleIdentifierForApplication(&bundleID, path))
365                    errexit("can't get bundle location for process '%s' (PSN %ld.%ld, pid %ld)", name, psn.hi, psn.lo, info->UnixPID);
366                if (bundleID != NULL) {
367                    char *bundleIDStr = (char *)CFStringGetCStringPtr(bundleID, CFStringGetSystemEncoding());
368                    if (bundleIDStr == NULL) {
369                        CFIndex bundleIDLength = CFStringGetLength(bundleID) + 1;
370                        bundleIDStr = (char *)malloc(bundleIDLength * sizeof(char));
371                        if (!CFStringGetCString(bundleID, bundleIDStr, bundleIDLength, CFStringGetSystemEncoding())) {
372                            CFShow(bundleIDStr);
373                            errexit("internal error: string encoding conversion failed for bundle identifier");
374                        }
375                        printf(" (%s)", bundleIDStr);
376                        free(bundleIDStr);
377                    } else {
378                        printf(" (%s)", bundleIDStr);
379                    }
380                    CFRelease(bundleID);
381                }
382            }
383            putchar('\n');
384            continue;
385        }
386        return psn;
387    }
388    if (err != procNotFound) osstatusexit(err, "can't get next process");
389
390    if (OPTS.appAction == APP_LIST) return frontApplication();
391
392    errexit("can't find matching process");
393    return psn;
394}
395
396int main (int argc, char * const argv[]) {
397    OSStatus err = noErr;
398
399    APP_NAME = argv[0];
400    getargs(argc, argv);
401
402    // need to establish connection with window server
403    InitCursor();
404
405    CPSProcessInfoRec info;
406    CPSProcessSerNum psn = matchApplication(&info);
407
408    const char *verb;
409    switch (OPTS.appAction) {
410        case APP_NONE: break;
411        case APP_LIST: break; // already handled in matchApplication
412        case APP_SWITCH: err = CPSSetFrontProcess(&psn); verb = "set front"; break;
413        case APP_SHOW: err = CPSPostShowReq(&psn); verb = "show"; break;
414        case APP_HIDE: err = CPSPostHideReq(&psn); verb = "hide"; break;
415        case APP_QUIT: err = quitApplication(&psn); verb = "quit"; break;
416        case APP_KILL: err = CPSPostKillRequest(&psn, kNilOptions); verb = "kill"; break;
417        case APP_KILL_HARD: err = CPSPostKillRequest(&psn, bfCPSKillHard); verb = "kill"; break;
418        case APP_PRINT_PID:
419            if (info.UnixPID <= 0) errexit("can't get process ID");
420            printf("%lu\n", info.UnixPID); // pid_t is signed, but this field isn't
421            break;
422        default:
423            errexit("internal error: invalid application action");
424    }
425    if (err != noErr) osstatusexit(err, "can't %s process", verb);
426
427    switch (OPTS.action) {
428        case ACTION_NONE: break;
429        case ACTION_SHOW_ALL: err = CPSPostShowAllReq(&psn); verb = "show all"; break;
430        case ACTION_HIDE_OTHERS: err = CPSPostHideMostReq(&psn); verb = "hide other"; break;
431        default:
432            errexit("internal error: invalid action");
433    }
434    if (err != noErr) osstatusexit(err, "can't %s processes", verb);
435
436    switch (OPTS.finalAction) {
437        case FINAL_NONE: break;
438        case FINAL_SWITCH:
439            psn = frontApplication();
440#if DEBUG
441            fprintf(stderr, "posting show request for %ld.%ld\n", psn.hi, psn.lo);
442#endif
443            if (OPTS.action != ACTION_NONE) usleep(750000); // XXX
444            err = CPSPostShowReq(&psn) || CPSSetFrontProcess(&psn);
445            verb = "bring current application's windows to the front";
446            break;
447        default:
448            errexit("internal error: invalid final action");   
449    }
450    if (err != noErr) osstatusexit(err, "can't %s", verb);
451
452    exit(0);
453}
Note: See TracBrowser for help on using the repository browser.