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,
291 static DBusGProxy *g_proxy = NULL;
292 GError *gerror = NULL;
295 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
296 "com.nokia.osso_browser",
297 "/com/nokia/osso_browser/request",
298 "com.nokia.osso_browser");
300 log_msg("Couldn't get a com.nokia.osso_browser proxy\n");
305 if (!strcmp(uri, "new_window")) {
306 if (flags & LAUNCH_MICROB_BOOKMARK_WIN_OK) {
307 if (!dbus_g_proxy_call(g_proxy, "top_application",
308 &gerror, G_TYPE_INVALID,
310 log_msg("Opening window failed: %s\n",
312 g_error_free(gerror);
318 /* Since we can't detect when the bookmark window
319 closes, we'd have a corner case where, if the user
320 just closes the bookmark window without opening any
321 browser windows, we don't kill off MicroB or resume
322 handling com.nokia.osso_browser */
326 if (!dbus_g_proxy_call(g_proxy, "open_new_window",
331 log_msg("Opening window failed: %s\n", gerror->message);
332 g_error_free(gerror);
339 /* Launch Fremantle MicroB and kill it when the session is finished */
340 void launch_microb_fremantle_with_kill(struct swb_context *ctx, char *uri) {
343 char *homedir, *microb_profile_dir, *microb_lockfile;
346 DBusConnection *raw_connection;
349 struct inotify_event *event;
350 pid_t browserd_pid, waited_pid;
351 struct sigaction act, oldact;
354 /* Put together the path to the MicroB browserd lockfile */
355 if (!(homedir = getenv("HOME")))
356 homedir = DEFAULT_HOMEDIR;
357 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
358 if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
359 log_msg("calloc() failed\n");
362 snprintf(microb_profile_dir, len, "%s%s",
363 homedir, MICROB_PROFILE_DIR);
364 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
365 strlen("/") + strlen(MICROB_LOCKFILE) + 1;
366 if (!(microb_lockfile = calloc(len, sizeof(char)))) {
367 log_msg("calloc() failed\n");
370 snprintf(microb_lockfile, len, "%s%s/%s",
371 homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
373 /* Watch for the creation of a MicroB browserd lockfile
374 NB: The watch has to be set up here, before the browser
375 is launched, to make sure there's no race between browserd
376 starting and us creating the watch */
377 if ((fd = inotify_init()) == -1) {
378 log_perror(errno, "inotify_init");
381 if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
383 log_perror(errno, "inotify_add_watch");
386 free(microb_profile_dir);
388 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
389 acquiring the com.nokia.osso_browser D-Bus name. Again, this needs
390 to happen before the browser is launched, so that there's no race
391 between establishing the watch and browser startup. */
392 if (!(raw_connection = microb_start_dbus_watch_init())) {
396 /* Launch a MicroB browser process if it's not already running */
397 if ((pid = launch_microb_start_browser_process(raw_connection, fd)) < 0)
400 /* Release the osso_browser D-Bus name so that MicroB can take it */
401 dbus_release_osso_browser_name(ctx);
403 /* Wait for our child to start the browser UI process and
404 for it to acquire the com.nokia.osso_browser D-Bus name,
405 then make the appropriate method call to open the browser
407 microb_start_dbus_watch_wait(raw_connection);
408 microb_start_dbus_watch_remove(raw_connection);
409 if (!launch_microb_open_window(ctx, uri, 0)) {
413 /* Workaround: the browser process we started is going to want
414 to hang around forever, hogging the com.nokia.osso_browser
415 D-Bus interface while at it. To fix this, we notice that
416 when the last browser window closes, the browser UI restarts
417 its attached browserd process. Get the browserd process's
418 PID and use ptrace() to watch for process termination.
420 This has the problem of not being able to detect whether
421 the bookmark window is open and/or in use, but it's the best
422 that I can think of. Better suggestions would be greatly
426 /* If we didn't start the MicroB browser process ourselves, try
427 to get the PID of the browserd from the lockfile */
428 browserd_pid = get_browserd_pid(microb_lockfile);
432 /* If getting the lockfile PID failed, or the lockfile PID doesn't
433 exist, assume that we have a stale lockfile and wait for the new
434 browserd lockfile to be created */
435 if (browserd_pid <= 0 || kill(browserd_pid, 0) == ESRCH) {
436 log_msg("Waiting for browserd lockfile to be created\n");
437 memset(buf, '\0', 256);
438 /* read() blocks until there are events to be read */
439 while ((bytes_read = read(fd, buf, 255)) > 0) {
441 /* Loop until we see the event we're looking for
442 or until all the events are processed */
443 while (pos && (pos-buf) < bytes_read) {
444 event = (struct inotify_event *)pos;
445 len = sizeof(struct inotify_event) + event->len;
446 if (!strcmp(MICROB_LOCKFILE, event->name)) {
447 /* Lockfile created */
450 } else if ((pos-buf) + len < bytes_read)
451 /* More events to process */
454 /* All events processed */
458 /* Event found, stop looking */
460 memset(buf, '\0', 256);
463 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
464 if (browserd_pid == 0)
465 log_msg("Profile lockfile link lacks PID\n");
467 log_perror(-browserd_pid,
468 "readlink() on lockfile failed");
472 inotify_rm_watch(fd, inot_wd);
474 free(microb_lockfile);
476 /* Wait for the browserd to close */
477 log_msg("Waiting for MicroB (browserd pid %d) to finish\n",
479 /* Clear any existing SIGCHLD handler to prevent interference
481 act.sa_handler = SIG_DFL;
483 sigemptyset(&(act.sa_mask));
484 if (sigaction(SIGCHLD, &act, &oldact) == -1) {
485 log_perror(errno, "clearing SIGCHLD handler failed");
489 /* Trace the browserd to get a close notification */
491 if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
492 log_perror(errno, "PTRACE_ATTACH");
495 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
496 while ((waited_pid = wait(&status)) > 0) {
497 if (waited_pid != browserd_pid)
498 /* Not interested in other processes */
500 if (WIFEXITED(status) || WIFSIGNALED(status))
501 /* browserd exited */
503 else if (WIFSTOPPED(status)) {
504 /* browserd was sent a signal
505 We're responsible for making sure this signal gets
507 if (ignore_sigstop && WSTOPSIG(status) == SIGSTOP) {
508 /* Ignore the first SIGSTOP received
509 This is raised for some reason immediately
510 after we start tracing the process, and
511 won't be followed by a SIGCONT at any point
513 log_msg("Ignoring first SIGSTOP\n");
514 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
518 log_msg("Forwarding signal %d to browserd\n",
520 ptrace(PTRACE_CONT, browserd_pid, NULL,
525 /* Kill off browser UI
526 XXX: There is a race here with the restarting of the closed
527 browserd; if that happens before we kill the browser UI, the newly
528 started browserd may not close with the UI
529 XXX: Hope we don't cause data loss here! */
530 log_msg("Killing MicroB\n");
533 waitpid(pid, &status, 0);
535 system("kill `pidof browser` > /dev/null 2>&1");
538 /* Restore old SIGCHLD handler */
539 if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
540 log_perror(errno, "restoring old SIGCHLD handler failed");
544 dbus_request_osso_browser_name(ctx);
547 /* Launch a new window in Fremantle MicroB; don't kill the MicroB process
548 when the session is finished
549 This is designed to work with a prestarted MicroB process that runs
550 continuously in the background */
551 void launch_microb_fremantle(struct swb_context *ctx, char *uri) {
552 DBusConnection *raw_connection;
554 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
555 acquiring the com.nokia.osso_browser D-Bus name */
556 if (!(raw_connection = microb_start_dbus_watch_init())) {
560 /* Launch a MicroB browser process if it's not already running */
561 if (launch_microb_start_browser_process(raw_connection, -1) < 0)
564 /* Release the osso_browser D-Bus name so that MicroB can take it */
565 dbus_release_osso_browser_name(ctx);
567 /* Wait for MicroB to acquire com.nokia.osso_browser, then make the
568 appropriate method call to open the browser window. */
569 microb_start_dbus_watch_wait(raw_connection);
570 microb_start_dbus_watch_remove(raw_connection);
571 if (!launch_microb_open_window(ctx, uri,
572 LAUNCH_MICROB_BOOKMARK_WIN_OK)) {
576 /* Take back the osso_browser D-Bus name from MicroB */
577 dbus_request_osso_browser_name(ctx);
579 #endif /* FREMANTLE */
581 void launch_microb(struct swb_context *ctx, char *uri) {
582 int kill_browserd = 0;
591 log_msg("launch_microb with uri '%s'\n", uri);
593 /* Launch browserd if it's not running */
594 status = system("pidof browserd > /dev/null");
595 if (WIFEXITED(status) && WEXITSTATUS(status)) {
598 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
600 system("/usr/sbin/browserd -d > /dev/null 2>&1");
605 /* Do the insanity to launch Fremantle MicroB */
606 if ((ctx->default_browser_launcher == launch_microb &&
607 ctx->autostart_microb) || ctx->autostart_microb == 1) {
609 /* If MicroB is set as the default browser, or if the user has
610 configured MicroB to always be running, just send the
611 running MicroB the request */
612 launch_microb_fremantle(ctx, uri);
614 /* Otherwise, launch MicroB and kill it when the user's
615 MicroB session is done */
616 launch_microb_fremantle_with_kill(ctx, uri);
618 #else /* !FREMANTLE */
619 /* Release the osso_browser D-Bus name so that MicroB can take it */
620 dbus_release_osso_browser_name(ctx);
622 if ((pid = fork()) == -1) {
623 log_perror(errno, "fork");
629 waitpid(pid, &status, 0);
634 /* exec maemo-invoker directly instead of relying on the
635 /usr/bin/browser symlink, since /usr/bin/browser may have
636 been replaced with a shell script calling us via D-Bus */
637 if (!strcmp(uri, "new_window")) {
638 execl("/usr/bin/maemo-invoker",
639 "browser", (char *)NULL);
641 execl("/usr/bin/maemo-invoker",
642 "browser", "--url", uri, (char *)NULL);
646 dbus_request_osso_browser_name(ctx);
647 #endif /* FREMANTLE */
649 /* Kill off browserd if we started it */
651 system("kill `pidof browserd`");
653 if (!ctx || !ctx->continuous_mode)
657 static void launch_other_browser(struct swb_context *ctx, char *uri) {
659 char *quoted_uri, *quote;
661 size_t cmdlen, urilen;
662 size_t quoted_uri_size;
665 if (!uri || !strcmp(uri, "new_window"))
668 log_msg("launch_other_browser with uri '%s'\n", uri);
670 if ((urilen = strlen(uri)) > 0) {
671 /* Quote the URI to prevent the shell from interpreting it */
672 /* urilen+3 = length of URI + 2x \' + \0 */
673 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
675 snprintf(quoted_uri, urilen+3, "'%s'", uri);
677 /* If there are any 's in the original URI, URL-escape them
678 (replace them with %27) */
679 quoted_uri_size = urilen + 3;
680 quote = quoted_uri + 1;
681 while ((quote = strchr(quote, '\'')) &&
682 (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
683 /* Check to make sure we don't shrink the memory area
684 as a result of integer overflow */
685 if (quoted_uri_size+2 <= quoted_uri_size)
688 /* Grow the memory area;
689 2 = strlen("%27")-strlen("'") */
690 if (!(quoted_uri = realloc(quoted_uri,
693 quoted_uri_size = quoted_uri_size + 2;
695 /* Recalculate the location of the ' character --
696 realloc() may have moved the string in memory */
697 quote = quoted_uri + offset;
699 /* Move the string after the ', including the \0,
701 memmove(quote+3, quote+1, strlen(quote));
702 memcpy(quote, "%27", 3);
705 urilen = strlen(quoted_uri);
709 cmdlen = strlen(ctx->other_browser_cmd);
711 /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
712 replace "%s"), but is needed in the case other_browser_cmd has no %s
714 if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
716 snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
717 log_msg("command: '%s'\n", command);
719 if (ctx->continuous_mode) {
721 /* Parent process or error in fork() */
731 execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
735 /* The list of known browsers and how to launch them */
736 static struct browser_launcher browser_launchers[] = {
737 { "microb", launch_microb, NULL, NULL }, /* First entry is the default! */
738 { "tear", launch_tear, NULL, "/usr/bin/tear" },
739 { "fennec", NULL, "fennec %s", "/usr/bin/fennec" },
740 { "opera", NULL, "opera %s", "/usr/bin/opera" },
741 { "midori", NULL, "midori %s", "/usr/bin/midori" },
742 { NULL, NULL, NULL, NULL },
745 static void use_launcher_as_default(struct swb_context *ctx,
746 struct browser_launcher *browser) {
747 if (!ctx || !browser)
750 if (browser->launcher)
751 ctx->default_browser_launcher = browser->launcher;
752 else if (browser->other_browser_cmd) {
753 free(ctx->other_browser_cmd);
755 /* Make a copy of the string constant so that
756 ctx->other_browser_cmd is safe to free() */
757 ctx->other_browser_cmd = strdup(browser->other_browser_cmd);
758 if (!ctx->other_browser_cmd) {
759 log_msg("malloc failed!\n");
760 /* Ideally, we'd configure the built-in default here --
761 but it's possible we could be called in that path */
764 ctx->default_browser_launcher = launch_other_browser;
770 void update_default_browser(struct swb_context *ctx, char *default_browser) {
771 struct browser_launcher *browser;
776 /* Configure the built-in default to start -- that way, we can
777 handle errors by just returning */
778 use_launcher_as_default(ctx, &browser_launchers[0]);
780 if (!default_browser)
781 /* No default_browser configured -- use built-in default */
784 /* Go through the list of known browser launchers and use one if
786 for (browser = browser_launchers; browser->name; ++browser)
787 if (!strcmp(default_browser, browser->name)) {
788 /* Make sure the user's choice is installed on the
790 if (browser->binary && access(browser->binary, X_OK)) {
791 log_msg("%s appears not to be installed\n",
794 use_launcher_as_default(ctx, browser);
799 /* Deal with default_browser = "other" */
800 if (!strcmp(default_browser, "other")) {
801 if (ctx->other_browser_cmd)
802 ctx->default_browser_launcher = launch_other_browser;
804 log_msg("default_browser is 'other', but no other_browser_cmd set -- using default\n");
808 /* Unknown value of default_browser */
809 log_msg("Unknown default_browser %s, using default\n", default_browser);
813 void launch_browser(struct swb_context *ctx, char *uri) {
814 if (ctx && ctx->default_browser_launcher)
815 ctx->default_browser_launcher(ctx, uri);