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

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

package-appswitch.sh: replace pbxbuild with xcodebuild; fix permissions

appswitch/appswitch.1: updated for actual release date

appswitch/main.c: updated for 1.0.1; fix PSN width, resolves #1; fix
compiler warning

appswitch/README: updated for actual release date; PB -> Xcode;
updated timings; updated version history

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 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.