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;
62 /* Close stdin/stdout/stderr and replace with /dev/null */
63 static int close_stdio(void) {
66 if ((fd = open("/dev/null", O_RDWR)) == -1)
69 if (dup2(fd, 0) == -1 || dup2(fd, 1) == -1 || dup2(fd, 2) == -1)
77 static void launch_tear(struct swb_context *ctx, char *uri) {
79 static DBusGProxy *tear_proxy = NULL;
86 log_msg("launch_tear with uri '%s'\n", uri);
88 /* We should be able to just call the D-Bus service to open Tear ...
89 but if Tear's not open, that cuases D-Bus to start Tear and then
90 pass it the OpenAddress call, which results in two browser windows.
91 Properly fixing this probably requires Tear to provide a D-Bus
92 method that opens an address in an existing window, but for now work
93 around by just invoking Tear with exec() if it's not running. */
94 status = system("pidof tear > /dev/null");
95 if (WIFEXITED(status) && !WEXITSTATUS(status)) {
97 if (!(tear_proxy = dbus_g_proxy_new_for_name(
101 "com.nokia.Tear"))) {
102 log_msg("Failed to create proxy for com.nokia.Tear D-Bus interface\n");
107 if (!dbus_g_proxy_call(tear_proxy, "OpenAddress", &error,
108 G_TYPE_STRING, uri, G_TYPE_INVALID,
110 log_msg("Opening window failed: %s\n", error->message);
113 if (!ctx->continuous_mode)
116 if (ctx->continuous_mode) {
117 if ((pid = fork()) != 0) {
118 /* Parent process or error in fork() */
119 log_msg("child: %d\n", (int)pid);
126 execl("/usr/bin/tear", "/usr/bin/tear", uri, (char *)NULL);
132 /* Get a browserd PID from the corresponding Mozilla profile lockfile */
133 static pid_t get_browserd_pid(const char *lockfile) {
136 /* The lockfile is a symlink pointing to "[ipaddr]:+[pid]", so read in
137 the target of the symlink and parse it that way */
138 memset(buf, '\0', 256);
139 if (readlink(lockfile, buf, 255) == -1)
141 if (!(tmp = strstr(buf, ":+")))
143 tmp += 2; /* Skip over the ":+" */
148 /* Check to see whether MicroB is ready to handle D-Bus requests yet
149 See the comments in microb_start_dbus_watch_* to understand how this
151 static DBusHandlerResult check_microb_started(DBusConnection *connection,
152 DBusMessage *message,
155 char *name, *old, *new;
157 log_msg("Checking to see if MicroB is ready\n");
158 dbus_error_init(&error);
159 if (!dbus_message_get_args(message, &error,
160 DBUS_TYPE_STRING, &name,
161 DBUS_TYPE_STRING, &old,
162 DBUS_TYPE_STRING, &new,
163 DBUS_TYPE_INVALID)) {
164 log_msg("%s\n", error.message);
165 dbus_error_free(&error);
166 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
168 /* If new is not an empty string, then the name has been acquired, and
169 MicroB should be ready to handle our request */
170 if (strlen(new) > 0) {
171 log_msg("MicroB ready\n");
175 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
178 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB acquiring the
179 com.nokia.osso_browser D-Bus name.
181 Ideas for how to do this monitoring derived from the dbus-monitor code
182 (tools/dbus-monitor.c in the D-Bus codebase). */
183 DBusConnection *microb_start_dbus_watch_init(void) {
184 DBusConnection *conn;
185 DBusError dbus_error;
186 DBusHandleMessageFunction filter_func = check_microb_started;
188 dbus_error_init(&dbus_error);
190 conn = dbus_bus_get_private(DBUS_BUS_SESSION, &dbus_error);
192 log_msg("Failed to open connection to session bus: %s\n",
194 dbus_error_free(&dbus_error);
198 dbus_bus_add_match(conn,
199 "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
201 if (dbus_error_is_set(&dbus_error)) {
202 log_msg("Failed to set up watch for browser UI start: %s\n",
204 dbus_error_free(&dbus_error);
207 if (!dbus_connection_add_filter(conn, filter_func, NULL, NULL)) {
208 log_msg("Failed to set up watch filter!\n");
215 /* Wait for MicroB to acquire the com.nokia.osso_browser D-Bus name
216 Blocks until name is acquired, then returns */
217 void microb_start_dbus_watch_wait(DBusConnection *conn) {
219 log_msg("Waiting for MicroB to start\n");
220 while (!microb_started &&
221 dbus_connection_read_write_dispatch(conn, -1));
224 /* Tear down the D-Bus watch for acquiring com.nokia.osso-browser */
225 void microb_start_dbus_watch_remove(DBusConnection *conn) {
226 DBusError dbus_error;
227 DBusHandleMessageFunction filter_func = check_microb_started;
229 dbus_error_init(&dbus_error);
231 dbus_connection_remove_filter(conn, filter_func, NULL);
232 dbus_bus_remove_match(conn,
233 "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
235 if (dbus_error_is_set(&dbus_error))
236 /* Don't really care -- about to disconnect from the
238 dbus_error_free(&dbus_error);
239 dbus_connection_close(conn);
240 dbus_connection_unref(conn);
243 /* Start a new MicroB browser process if one isn't already running */
244 pid_t launch_microb_start_browser_process(DBusConnection *conn, int fd) {
248 status = system("pidof browser > /dev/null");
249 if (WIFEXITED(status) && !WEXITSTATUS(status)) {
250 /* MicroB browser already running */
254 if ((pid = fork()) == -1) {
255 log_perror(errno, "fork");
261 dbus_connection_close(conn);
262 dbus_connection_unref(conn);
267 /* exec maemo-invoker directly instead of relying on the
268 /usr/bin/browser symlink, since /usr/bin/browser may have
269 been replaced with a shell script calling us via D-Bus */
270 /* Launch the browser in the background -- our parent will
271 wait for it to claim the D-Bus name and then display the
272 window using D-Bus */
273 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
275 /* If we get here, exec() failed */
282 /* Open a MicroB window using the D-Bus interface
283 It's assumed that we have already released the D-Bus name and that it's been
284 ensured that MicroB has acquired com.nokia.osso_browser (otherwise this will
285 cause D-Bus to try forever to launch another browser-switchboard) */
287 #define LAUNCH_MICROB_BOOKMARK_WIN_OK 0x1
289 int launch_microb_open_window(struct swb_context *ctx, char *uri,
292 GError *gerror = NULL;
294 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
295 "com.nokia.osso_browser",
296 "/com/nokia/osso_browser/request",
297 "com.nokia.osso_browser");
299 log_msg("Couldn't get a com.nokia.osso_browser proxy\n");
303 if (!strcmp(uri, "new_window")) {
304 if (flags & LAUNCH_MICROB_BOOKMARK_WIN_OK) {
305 if (!dbus_g_proxy_call(g_proxy, "top_application",
306 &gerror, G_TYPE_INVALID,
308 log_msg("Opening window failed: %s\n",
310 g_error_free(gerror);
316 /* Since we can't detect when the bookmark window
317 closes, we'd have a corner case where, if the user
318 just closes the bookmark window without opening any
319 browser windows, we don't kill off MicroB or resume
320 handling com.nokia.osso_browser */
324 if (!dbus_g_proxy_call(g_proxy, "load_url",
329 log_msg("Opening window failed: %s\n", gerror->message);
330 g_error_free(gerror);
334 g_object_unref(g_proxy);
338 /* Launch Fremantle MicroB and kill it when the session is finished */
339 void launch_microb_fremantle_with_kill(struct swb_context *ctx, char *uri) {
342 char *homedir, *microb_profile_dir, *microb_lockfile;
345 DBusConnection *raw_connection;
348 struct inotify_event *event;
349 pid_t browserd_pid, waited_pid;
350 struct sigaction act, oldact;
353 /* Put together the path to the MicroB browserd lockfile */
354 if (!(homedir = getenv("HOME")))
355 homedir = DEFAULT_HOMEDIR;
356 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
357 if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
358 log_msg("calloc() failed\n");
361 snprintf(microb_profile_dir, len, "%s%s",
362 homedir, MICROB_PROFILE_DIR);
363 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
364 strlen("/") + strlen(MICROB_LOCKFILE) + 1;
365 if (!(microb_lockfile = calloc(len, sizeof(char)))) {
366 log_msg("calloc() failed\n");
369 snprintf(microb_lockfile, len, "%s%s/%s",
370 homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
372 /* Watch for the creation of a MicroB browserd lockfile
373 NB: The watch has to be set up here, before the browser
374 is launched, to make sure there's no race between browserd
375 starting and us creating the watch */
376 if ((fd = inotify_init()) == -1) {
377 log_perror(errno, "inotify_init");
380 if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
382 log_perror(errno, "inotify_add_watch");
385 free(microb_profile_dir);
387 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
388 acquiring the com.nokia.osso_browser D-Bus name. Again, this needs
389 to happen before the browser is launched, so that there's no race
390 between establishing the watch and browser startup. */
391 if (!(raw_connection = microb_start_dbus_watch_init())) {
395 /* Launch a MicroB browser process if it's not already running */
396 if ((pid = launch_microb_start_browser_process(raw_connection, fd)) < 0)
399 /* Release the osso_browser D-Bus name so that MicroB can take it */
400 dbus_release_osso_browser_name(ctx);
402 /* Wait for our child to start the browser UI process and
403 for it to acquire the com.nokia.osso_browser D-Bus name,
404 then make the appropriate method call to open the browser
406 microb_start_dbus_watch_wait(raw_connection);
407 microb_start_dbus_watch_remove(raw_connection);
408 if (!launch_microb_open_window(ctx, uri, 0)) {
412 /* Workaround: the browser process we started is going to want
413 to hang around forever, hogging the com.nokia.osso_browser
414 D-Bus interface while at it. To fix this, we notice that
415 when the last browser window closes, the browser UI restarts
416 its attached browserd process. Get the browserd process's
417 PID and use ptrace() to watch for process termination.
419 This has the problem of not being able to detect whether
420 the bookmark window is open and/or in use, but it's the best
421 that I can think of. Better suggestions would be greatly
425 /* If we didn't start the MicroB browser process ourselves, try
426 to get the PID of the browserd from the lockfile */
427 browserd_pid = get_browserd_pid(microb_lockfile);
431 /* If getting the lockfile PID failed, or the lockfile PID doesn't
432 exist, assume that we have a stale lockfile and wait for the new
433 browserd lockfile to be created */
434 if (browserd_pid <= 0 || kill(browserd_pid, 0) == ESRCH) {
435 log_msg("Waiting for browserd lockfile to be created\n");
436 memset(buf, '\0', 256);
437 /* read() blocks until there are events to be read */
438 while ((bytes_read = read(fd, buf, 255)) > 0) {
440 /* Loop until we see the event we're looking for
441 or until all the events are processed */
442 while (pos && (pos-buf) < bytes_read) {
443 event = (struct inotify_event *)pos;
444 len = sizeof(struct inotify_event) + event->len;
445 if (!strcmp(MICROB_LOCKFILE, event->name)) {
446 /* Lockfile created */
449 } else if ((pos-buf) + len < bytes_read)
450 /* More events to process */
453 /* All events processed */
457 /* Event found, stop looking */
459 memset(buf, '\0', 256);
462 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
463 if (browserd_pid == 0)
464 log_msg("Profile lockfile link lacks PID\n");
466 log_perror(-browserd_pid,
467 "readlink() on lockfile failed");
471 inotify_rm_watch(fd, inot_wd);
473 free(microb_lockfile);
475 /* Wait for the browserd to close */
476 log_msg("Waiting for MicroB (browserd pid %d) to finish\n",
478 /* Clear any existing SIGCHLD handler to prevent interference
480 act.sa_handler = SIG_DFL;
482 sigemptyset(&(act.sa_mask));
483 if (sigaction(SIGCHLD, &act, &oldact) == -1) {
484 log_perror(errno, "clearing SIGCHLD handler failed");
488 /* Trace the browserd to get a close notification */
490 if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
491 log_perror(errno, "PTRACE_ATTACH");
494 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
495 while ((waited_pid = wait(&status)) > 0) {
496 if (waited_pid != browserd_pid)
497 /* Not interested in other processes */
499 if (WIFEXITED(status) || WIFSIGNALED(status))
500 /* browserd exited */
502 else if (WIFSTOPPED(status)) {
503 /* browserd was sent a signal
504 We're responsible for making sure this signal gets
506 if (ignore_sigstop && WSTOPSIG(status) == SIGSTOP) {
507 /* Ignore the first SIGSTOP received
508 This is raised for some reason immediately
509 after we start tracing the process, and
510 won't be followed by a SIGCONT at any point
512 log_msg("Ignoring first SIGSTOP\n");
513 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
517 log_msg("Forwarding signal %d to browserd\n",
519 ptrace(PTRACE_CONT, browserd_pid, NULL,
524 /* Kill off browser UI
525 XXX: There is a race here with the restarting of the closed
526 browserd; if that happens before we kill the browser UI, the newly
527 started browserd may not close with the UI
528 XXX: Hope we don't cause data loss here! */
529 log_msg("Killing MicroB\n");
532 waitpid(pid, &status, 0);
534 system("kill `pidof browser` > /dev/null 2>&1");
537 /* Restore old SIGCHLD handler */
538 if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
539 log_perror(errno, "restoring old SIGCHLD handler failed");
543 dbus_request_osso_browser_name(ctx);
546 /* Launch a new window in Fremantle MicroB; don't kill the MicroB process
547 when the session is finished
548 This is designed to work with a prestarted MicroB process that runs
549 continuously in the background */
550 void launch_microb_fremantle(struct swb_context *ctx, char *uri) {
551 DBusConnection *raw_connection;
553 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
554 acquiring the com.nokia.osso_browser D-Bus name */
555 if (!(raw_connection = microb_start_dbus_watch_init())) {
559 /* Launch a MicroB browser process if it's not already running */
560 if (launch_microb_start_browser_process(raw_connection, -1) < 0)
563 /* Release the osso_browser D-Bus name so that MicroB can take it */
564 dbus_release_osso_browser_name(ctx);
566 /* Wait for MicroB to acquire com.nokia.osso_browser, then make the
567 appropriate method call to open the browser window. */
568 microb_start_dbus_watch_wait(raw_connection);
569 microb_start_dbus_watch_remove(raw_connection);
570 if (!launch_microb_open_window(ctx, uri,
571 LAUNCH_MICROB_BOOKMARK_WIN_OK)) {
575 /* Take back the osso_browser D-Bus name from MicroB */
576 dbus_request_osso_browser_name(ctx);
578 #endif /* FREMANTLE */
580 void launch_microb(struct swb_context *ctx, char *uri) {
581 int kill_browserd = 0;
590 log_msg("launch_microb with uri '%s'\n", uri);
592 /* Launch browserd if it's not running */
593 status = system("pidof browserd > /dev/null");
594 if (WIFEXITED(status) && WEXITSTATUS(status)) {
597 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
599 system("/usr/sbin/browserd -d > /dev/null 2>&1");
604 /* Do the insanity to launch Fremantle MicroB */
605 if ((ctx->default_browser_launcher == launch_microb &&
606 ctx->autostart_microb) || ctx->autostart_microb == 1) {
608 /* If MicroB is set as the default browser, or if the user has
609 configured MicroB to always be running, just send the
610 running MicroB the request */
611 launch_microb_fremantle(ctx, uri);
613 /* Otherwise, launch MicroB and kill it when the user's
614 MicroB session is done */
615 launch_microb_fremantle_with_kill(ctx, uri);
617 #else /* !FREMANTLE */
618 /* Release the osso_browser D-Bus name so that MicroB can take it */
619 dbus_release_osso_browser_name(ctx);
621 if ((pid = fork()) == -1) {
622 log_perror(errno, "fork");
628 waitpid(pid, &status, 0);
633 /* exec maemo-invoker directly instead of relying on the
634 /usr/bin/browser symlink, since /usr/bin/browser may have
635 been replaced with a shell script calling us via D-Bus */
636 if (!strcmp(uri, "new_window")) {
637 execl("/usr/bin/maemo-invoker",
638 "browser", (char *)NULL);
640 execl("/usr/bin/maemo-invoker",
641 "browser", "--url", uri, (char *)NULL);
645 dbus_request_osso_browser_name(ctx);
646 #endif /* FREMANTLE */
648 /* Kill off browserd if we started it */
650 system("kill `pidof browserd`");
652 if (!ctx || !ctx->continuous_mode)
656 static void launch_other_browser(struct swb_context *ctx, char *uri) {
658 char *quoted_uri, *quote;
660 size_t cmdlen, urilen;
661 size_t quoted_uri_size;
664 if (!uri || !strcmp(uri, "new_window"))
667 log_msg("launch_other_browser with uri '%s'\n", uri);
669 if ((urilen = strlen(uri)) > 0) {
670 /* Quote the URI to prevent the shell from interpreting it */
671 /* urilen+3 = length of URI + 2x \' + \0 */
672 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
674 snprintf(quoted_uri, urilen+3, "'%s'", uri);
676 /* If there are any 's in the original URI, URL-escape them
677 (replace them with %27) */
678 quoted_uri_size = urilen + 3;
679 quote = quoted_uri + 1;
680 while ((quote = strchr(quote, '\'')) &&
681 (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
682 /* Check to make sure we don't shrink the memory area
683 as a result of integer overflow */
684 if (quoted_uri_size+2 <= quoted_uri_size)
687 /* Grow the memory area;
688 2 = strlen("%27")-strlen("'") */
689 if (!(quoted_uri = realloc(quoted_uri,
692 quoted_uri_size = quoted_uri_size + 2;
694 /* Recalculate the location of the ' character --
695 realloc() may have moved the string in memory */
696 quote = quoted_uri + offset;
698 /* Move the string after the ', including the \0,
700 memmove(quote+3, quote+1, strlen(quote));
701 memcpy(quote, "%27", 3);
704 urilen = strlen(quoted_uri);
708 cmdlen = strlen(ctx->other_browser_cmd);
710 /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
711 replace "%s"), but is needed in the case other_browser_cmd has no %s
713 if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
715 snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
716 log_msg("command: '%s'\n", command);
718 if (ctx->continuous_mode) {
720 /* Parent process or error in fork() */
730 execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
734 /* The list of known browsers and how to launch them */
735 static struct browser_launcher browser_launchers[] = {
736 { "microb", launch_microb, NULL, NULL }, /* First entry is the default! */
737 { "tear", launch_tear, NULL, "/usr/bin/tear" },
738 { "fennec", NULL, "fennec %s", "/usr/bin/fennec" },
739 { "opera", NULL, "opera %s", "/usr/bin/opera" },
740 { "midori", NULL, "midori %s", "/usr/bin/midori" },
741 { NULL, NULL, NULL, NULL },
744 static void use_launcher_as_default(struct swb_context *ctx,
745 struct browser_launcher *browser) {
746 if (!ctx || !browser)
749 if (browser->launcher)
750 ctx->default_browser_launcher = browser->launcher;
751 else if (browser->other_browser_cmd) {
752 free(ctx->other_browser_cmd);
754 /* Make a copy of the string constant so that
755 ctx->other_browser_cmd is safe to free() */
756 ctx->other_browser_cmd = strdup(browser->other_browser_cmd);
757 if (!ctx->other_browser_cmd) {
758 log_msg("malloc failed!\n");
759 /* Ideally, we'd configure the built-in default here --
760 but it's possible we could be called in that path */
763 ctx->default_browser_launcher = launch_other_browser;
769 void update_default_browser(struct swb_context *ctx, char *default_browser) {
770 struct browser_launcher *browser;
775 /* Configure the built-in default to start -- that way, we can
776 handle errors by just returning */
777 use_launcher_as_default(ctx, &browser_launchers[0]);
779 if (!default_browser)
780 /* No default_browser configured -- use built-in default */
783 /* Go through the list of known browser launchers and use one if
785 for (browser = browser_launchers; browser->name; ++browser)
786 if (!strcmp(default_browser, browser->name)) {
787 /* Make sure the user's choice is installed on the
789 if (browser->binary && access(browser->binary, X_OK)) {
790 log_msg("%s appears not to be installed\n",
793 use_launcher_as_default(ctx, browser);
798 /* Deal with default_browser = "other" */
799 if (!strcmp(default_browser, "other")) {
800 if (ctx->other_browser_cmd)
801 ctx->default_browser_launcher = launch_other_browser;
803 log_msg("default_browser is 'other', but no other_browser_cmd set -- using default\n");
807 /* Unknown value of default_browser */
808 log_msg("Unknown default_browser %s, using default\n", default_browser);
812 void launch_browser(struct swb_context *ctx, char *uri) {
813 if (ctx && ctx->default_browser_launcher)
814 ctx->default_browser_launcher(ctx, uri);