2 * launcher.c -- functions for launching web browsers for browser-switchboard
4 * Copyright (C) 2009-2010 Steven Luo
5 * Derived from a Python implementation by Jason Simpson and Steven Luo
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
28 #include <sys/types.h>
32 #include <dbus/dbus-glib.h>
35 #include <dbus/dbus.h>
37 #include <sys/ptrace.h>
38 #include <sys/inotify.h>
40 #define DEFAULT_HOMEDIR "/home/user"
41 #define MICROB_PROFILE_DIR "/.mozilla/microb"
42 #define MICROB_LOCKFILE "lock"
45 #include "browser-switchboard.h"
47 #include "dbus-server-bindings.h"
50 struct browser_launcher {
52 void (*launcher)(struct swb_context *, char *);
53 char *other_browser_cmd;
58 static int microb_started = 0;
60 /* Check to see whether MicroB is ready to handle D-Bus requests yet
61 See the comments in launch_microb to understand how this works. */
62 static DBusHandlerResult check_microb_started(DBusConnection *connection,
66 char *name, *old, *new;
68 log_msg("Checking to see if MicroB is ready\n");
69 dbus_error_init(&error);
70 if (!dbus_message_get_args(message, &error,
71 DBUS_TYPE_STRING, &name,
72 DBUS_TYPE_STRING, &old,
73 DBUS_TYPE_STRING, &new,
75 log_msg("%s\n", error.message);
76 dbus_error_free(&error);
77 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
79 /* If old is an empty string, then the name has been acquired, and
80 MicroB should be ready to handle our request */
81 if (strlen(old) == 0) {
82 log_msg("MicroB ready\n");
86 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
89 /* Get a browserd PID from the corresponding Mozilla profile lockfile */
90 static pid_t get_browserd_pid(const char *lockfile) {
93 /* The lockfile is a symlink pointing to "[ipaddr]:+[pid]", so read in
94 the target of the symlink and parse it that way */
95 memset(buf, '\0', 256);
96 if (readlink(lockfile, buf, 255) == -1)
98 if (!(tmp = strstr(buf, ":+")))
100 tmp += 2; /* Skip over the ":+" */
106 /* Close stdin/stdout/stderr and replace with /dev/null */
107 static int close_stdio(void) {
110 if ((fd = open("/dev/null", O_RDWR)) == -1)
113 if (dup2(fd, 0) == -1 || dup2(fd, 1) == -1 || dup2(fd, 2) == -1)
120 static void launch_tear(struct swb_context *ctx, char *uri) {
122 static DBusGProxy *tear_proxy = NULL;
123 GError *error = NULL;
129 log_msg("launch_tear with uri '%s'\n", uri);
131 /* We should be able to just call the D-Bus service to open Tear ...
132 but if Tear's not open, that cuases D-Bus to start Tear and then
133 pass it the OpenAddress call, which results in two browser windows.
134 Properly fixing this probably requires Tear to provide a D-Bus
135 method that opens an address in an existing window, but for now work
136 around by just invoking Tear with exec() if it's not running. */
137 status = system("pidof tear > /dev/null");
138 if (WIFEXITED(status) && !WEXITSTATUS(status)) {
140 if (!(tear_proxy = dbus_g_proxy_new_for_name(
144 "com.nokia.Tear"))) {
145 log_msg("Failed to create proxy for com.nokia.Tear D-Bus interface\n");
150 if (!dbus_g_proxy_call(tear_proxy, "OpenAddress", &error,
151 G_TYPE_STRING, uri, G_TYPE_INVALID,
153 log_msg("Opening window failed: %s\n", error->message);
156 if (!ctx->continuous_mode)
159 if (ctx->continuous_mode) {
160 if ((pid = fork()) != 0) {
161 /* Parent process or error in fork() */
162 log_msg("child: %d\n", (int)pid);
169 execl("/usr/bin/tear", "/usr/bin/tear", uri, (char *)NULL);
173 void launch_microb(struct swb_context *ctx, char *uri) {
174 int kill_browserd = 0;
178 char *homedir, *microb_profile_dir, *microb_lockfile;
181 DBusConnection *raw_connection;
182 DBusError dbus_error;
183 DBusHandleMessageFunction filter_func;
185 GError *gerror = NULL;
188 struct inotify_event *event;
189 pid_t browserd_pid, waited_pid;
190 struct sigaction act, oldact;
197 log_msg("launch_microb with uri '%s'\n", uri);
199 /* Launch browserd if it's not running */
200 status = system("pidof browserd > /dev/null");
201 if (WIFEXITED(status) && WEXITSTATUS(status)) {
204 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
206 system("/usr/sbin/browserd -d > /dev/null 2>&1");
210 /* Release the osso_browser D-Bus name so that MicroB can take it */
211 dbus_release_osso_browser_name(ctx);
214 /* Put together the path to the MicroB browserd lockfile */
215 if (!(homedir = getenv("HOME")))
216 homedir = DEFAULT_HOMEDIR;
217 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
218 if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
219 log_msg("calloc() failed\n");
222 snprintf(microb_profile_dir, len, "%s%s",
223 homedir, MICROB_PROFILE_DIR);
224 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
225 strlen("/") + strlen(MICROB_LOCKFILE) + 1;
226 if (!(microb_lockfile = calloc(len, sizeof(char)))) {
227 log_msg("calloc() failed\n");
230 snprintf(microb_lockfile, len, "%s%s/%s",
231 homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
233 /* Watch for the creation of a MicroB browserd lockfile
234 NB: The watch has to be set up here, before the browser
235 is launched, to make sure there's no race between browserd
236 starting and us creating the watch */
237 if ((fd = inotify_init()) == -1) {
238 log_perror(errno, "inotify_init");
241 if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
243 log_perror(errno, "inotify_add_watch");
246 free(microb_profile_dir);
248 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
249 acquiring the com.nokia.osso_browser D-Bus name. Again, this needs
250 to happen before the browser is launched, so that there's no race
251 between establishing the watch and browser startup.
253 Ideas for how to do this monitoring derived from the dbus-monitor
254 code (tools/dbus-monitor.c in the D-Bus codebase). */
255 dbus_error_init(&dbus_error);
257 raw_connection = dbus_bus_get_private(DBUS_BUS_SESSION, &dbus_error);
258 if (!raw_connection) {
259 log_msg("Failed to open connection to session bus: %s\n",
261 dbus_error_free(&dbus_error);
265 dbus_bus_add_match(raw_connection,
266 "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
268 if (dbus_error_is_set(&dbus_error)) {
269 log_msg("Failed to set up watch for browser UI start: %s\n",
271 dbus_error_free(&dbus_error);
274 filter_func = check_microb_started;
275 if (!dbus_connection_add_filter(raw_connection,
276 filter_func, NULL, NULL)) {
277 log_msg("Failed to set up watch filter!\n");
281 if ((pid = fork()) == -1) {
282 log_perror(errno, "fork");
288 /* Wait for our child to start the browser UI process and
289 for it to acquire the com.nokia.osso_browser D-Bus name,
290 then make the appropriate method call to open the browser
293 log_msg("Waiting for MicroB to start\n");
294 while (!microb_started &&
295 dbus_connection_read_write_dispatch(raw_connection,
297 dbus_connection_remove_filter(raw_connection,
299 dbus_bus_remove_match(raw_connection,
300 "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
302 if (dbus_error_is_set(&dbus_error))
303 /* Don't really care -- about to disconnect from the
305 dbus_error_free(&dbus_error);
306 dbus_connection_close(raw_connection);
307 dbus_connection_unref(raw_connection);
309 /* Browser UI's started, send it the request for a new window
311 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
312 "com.nokia.osso_browser",
313 "/com/nokia/osso_browser/request",
314 "com.nokia.osso_browser");
316 log_msg("Couldn't get a com.nokia.osso_browser proxy\n");
319 if (!strcmp(uri, "new_window")) {
320 #if 0 /* Since we can't detect when the bookmark window closes, we'd have a
321 corner case where, if the user just closes the bookmark window
322 without opening any browser windows, we don't kill off MicroB or
323 resume handling com.nokia.osso_browser */
324 if (!dbus_g_proxy_call(g_proxy, "top_application",
325 &gerror, G_TYPE_INVALID,
327 log_msg("Opening window failed: %s\n",
332 if (!dbus_g_proxy_call(g_proxy, "load_url",
334 G_TYPE_STRING, "about:blank",
337 log_msg("Opening window failed: %s\n",
342 if (!dbus_g_proxy_call(g_proxy, "load_url",
347 log_msg("Opening window failed: %s\n",
352 g_object_unref(g_proxy);
354 /* Workaround: the browser process we started is going to want
355 to hang around forever, hogging the com.nokia.osso_browser
356 D-Bus interface while at it. To fix this, we notice that
357 when the last browser window closes, the browser UI restarts
358 its attached browserd process. Get the browserd process's
359 PID and use ptrace() to watch for process termination.
361 This has the problem of not being able to detect whether
362 the bookmark window is open and/or in use, but it's the best
363 that I can think of. Better suggestions would be greatly
366 /* Wait for the MicroB browserd lockfile to be created */
367 log_msg("Waiting for browserd lockfile to be created\n");
368 memset(buf, '\0', 256);
369 /* read() blocks until there are events to be read */
370 while ((bytes_read = read(fd, buf, 255)) > 0) {
372 /* Loop until we see the event we're looking for
373 or until all the events are processed */
374 while (pos && (pos-buf) < bytes_read) {
375 event = (struct inotify_event *)pos;
376 len = sizeof(struct inotify_event)
378 if (!strcmp(MICROB_LOCKFILE,
380 /* Lockfile created */
383 } else if ((pos-buf) + len < bytes_read)
384 /* More events to process */
387 /* All events processed */
391 /* Event found, stop looking */
393 memset(buf, '\0', 256);
395 inotify_rm_watch(fd, inot_wd);
398 /* Get the PID of the browserd from the lockfile */
399 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
400 if (browserd_pid == 0)
401 log_msg("Profile lockfile link lacks PID\n");
403 log_perror(-browserd_pid,
404 "readlink() on lockfile failed");
407 free(microb_lockfile);
409 /* Wait for the browserd to close */
410 log_msg("Waiting for MicroB (browserd pid %d) to finish\n",
412 /* Clear any existing SIGCHLD handler to prevent interference
414 act.sa_handler = SIG_DFL;
416 sigemptyset(&(act.sa_mask));
417 if (sigaction(SIGCHLD, &act, &oldact) == -1) {
418 log_perror(errno, "clearing SIGCHLD handler failed");
422 /* Trace the browserd to get a close notification */
424 if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
425 log_perror(errno, "PTRACE_ATTACH");
428 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
429 while ((waited_pid = wait(&status)) > 0) {
430 if (waited_pid != browserd_pid)
431 /* Not interested in other processes */
433 if (WIFEXITED(status) || WIFSIGNALED(status))
434 /* browserd exited */
436 else if (WIFSTOPPED(status)) {
437 /* browserd was sent a signal
438 We're responsible for making sure this
439 signal gets delivered */
440 if (ignore_sigstop &&
441 WSTOPSIG(status) == SIGSTOP) {
442 /* Ignore the first SIGSTOP received
443 This is raised for some reason
444 immediately after we start tracing
445 the process, and won't be followed
446 by a SIGCONT at any point */
447 log_msg("Ignoring first SIGSTOP\n");
448 ptrace(PTRACE_CONT, browserd_pid,
453 log_msg("Forwarding signal %d to browserd\n",
455 ptrace(PTRACE_CONT, browserd_pid,
456 NULL, WSTOPSIG(status));
460 /* Kill off browser UI
461 XXX: There is a race here with the restarting of the closed
462 browserd; if that happens before we kill the browser UI, the
463 newly started browserd may not close with the UI
464 XXX: Hope we don't cause data loss here! */
465 log_msg("Killing MicroB\n");
467 waitpid(pid, &status, 0);
469 /* Restore old SIGCHLD handler */
470 if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
472 "restoring old SIGCHLD handler failed");
477 dbus_connection_close(raw_connection);
478 dbus_connection_unref(raw_connection);
482 /* exec maemo-invoker directly instead of relying on the
483 /usr/bin/browser symlink, since /usr/bin/browser may have
484 been replaced with a shell script calling us via D-Bus */
485 /* Launch the browser in the background -- our parent will
486 wait for it to claim the D-Bus name and then display the
487 window using D-Bus */
488 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
490 #else /* !FREMANTLE */
491 if ((pid = fork()) == -1) {
492 log_perror(errno, "fork");
498 waitpid(pid, &status, 0);
503 /* exec maemo-invoker directly instead of relying on the
504 /usr/bin/browser symlink, since /usr/bin/browser may have
505 been replaced with a shell script calling us via D-Bus */
506 if (!strcmp(uri, "new_window")) {
507 execl("/usr/bin/maemo-invoker",
508 "browser", (char *)NULL);
510 execl("/usr/bin/maemo-invoker",
511 "browser", "--url", uri, (char *)NULL);
514 #endif /* FREMANTLE */
516 /* Kill off browserd if we started it */
518 system("kill `pidof browserd`");
520 if (!ctx || !ctx->continuous_mode)
523 dbus_request_osso_browser_name(ctx);
526 static void launch_other_browser(struct swb_context *ctx, char *uri) {
528 char *quoted_uri, *quote;
530 size_t cmdlen, urilen;
531 size_t quoted_uri_size;
534 if (!uri || !strcmp(uri, "new_window"))
537 log_msg("launch_other_browser with uri '%s'\n", uri);
539 if ((urilen = strlen(uri)) > 0) {
540 /* Quote the URI to prevent the shell from interpreting it */
541 /* urilen+3 = length of URI + 2x \' + \0 */
542 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
544 snprintf(quoted_uri, urilen+3, "'%s'", uri);
546 /* If there are any 's in the original URI, URL-escape them
547 (replace them with %27) */
548 quoted_uri_size = urilen + 3;
549 quote = quoted_uri + 1;
550 while ((quote = strchr(quote, '\'')) &&
551 (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
552 /* Check to make sure we don't shrink the memory area
553 as a result of integer overflow */
554 if (quoted_uri_size+2 <= quoted_uri_size)
557 /* Grow the memory area;
558 2 = strlen("%27")-strlen("'") */
559 if (!(quoted_uri = realloc(quoted_uri,
562 quoted_uri_size = quoted_uri_size + 2;
564 /* Recalculate the location of the ' character --
565 realloc() may have moved the string in memory */
566 quote = quoted_uri + offset;
568 /* Move the string after the ', including the \0,
570 memmove(quote+3, quote+1, strlen(quote));
571 memcpy(quote, "%27", 3);
574 urilen = strlen(quoted_uri);
578 cmdlen = strlen(ctx->other_browser_cmd);
580 /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
581 replace "%s"), but is needed in the case other_browser_cmd has no %s
583 if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
585 snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
586 log_msg("command: '%s'\n", command);
588 if (ctx->continuous_mode) {
590 /* Parent process or error in fork() */
600 execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
604 /* The list of known browsers and how to launch them */
605 static struct browser_launcher browser_launchers[] = {
606 { "microb", launch_microb, NULL, NULL }, /* First entry is the default! */
607 { "tear", launch_tear, NULL, "/usr/bin/tear" },
608 { "fennec", NULL, "fennec %s", "/usr/bin/fennec" },
609 { "opera", NULL, "opera %s", "/usr/bin/opera" },
610 { "midori", NULL, "midori %s", "/usr/bin/midori" },
611 { NULL, NULL, NULL, NULL },
614 static void use_launcher_as_default(struct swb_context *ctx,
615 struct browser_launcher *browser) {
616 if (!ctx || !browser)
619 if (browser->launcher)
620 ctx->default_browser_launcher = browser->launcher;
621 else if (browser->other_browser_cmd) {
622 free(ctx->other_browser_cmd);
624 /* Make a copy of the string constant so that
625 ctx->other_browser_cmd is safe to free() */
626 ctx->other_browser_cmd = strdup(browser->other_browser_cmd);
627 if (!ctx->other_browser_cmd) {
628 log_msg("malloc failed!\n");
629 /* Ideally, we'd configure the built-in default here --
630 but it's possible we could be called in that path */
633 ctx->default_browser_launcher = launch_other_browser;
639 void update_default_browser(struct swb_context *ctx, char *default_browser) {
640 struct browser_launcher *browser;
645 /* Configure the built-in default to start -- that way, we can
646 handle errors by just returning */
647 use_launcher_as_default(ctx, &browser_launchers[0]);
649 if (!default_browser)
650 /* No default_browser configured -- use built-in default */
653 /* Go through the list of known browser launchers and use one if
655 for (browser = browser_launchers; browser->name; ++browser)
656 if (!strcmp(default_browser, browser->name)) {
657 /* Make sure the user's choice is installed on the
659 if (browser->binary && !access(browser->binary, X_OK)) {
660 use_launcher_as_default(ctx, browser);
663 log_msg("%s appears not to be installed\n",
667 /* Deal with default_browser = "other" */
668 if (!strcmp(default_browser, "other")) {
669 if (ctx->other_browser_cmd)
670 ctx->default_browser_launcher = launch_other_browser;
672 log_msg("default_browser is 'other', but no other_browser_cmd set -- using default\n");
676 /* Unknown value of default_browser */
677 log_msg("Unknown default_browser %s, using default\n", default_browser);
681 void launch_browser(struct swb_context *ctx, char *uri) {
682 if (ctx && ctx->default_browser_launcher)
683 ctx->default_browser_launcher(ctx, uri);