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_connection_remove_filter(conn, filter_func, NULL);
230 dbus_bus_remove_match(conn,
231 "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
233 if (dbus_error_is_set(&dbus_error))
234 /* Don't really care -- about to disconnect from the
236 dbus_error_free(&dbus_error);
237 dbus_connection_close(conn);
238 dbus_connection_unref(conn);
241 /* Start a new MicroB browser process if one isn't already running */
242 pid_t launch_microb_start_browser_process(DBusConnection *conn, int fd) {
246 status = system("pidof browser > /dev/null");
247 if (WIFEXITED(status) && !WEXITSTATUS(status)) {
248 /* MicroB browser already running */
252 if ((pid = fork()) == -1) {
253 log_perror(errno, "fork");
259 dbus_connection_close(conn);
260 dbus_connection_unref(conn);
265 /* exec maemo-invoker directly instead of relying on the
266 /usr/bin/browser symlink, since /usr/bin/browser may have
267 been replaced with a shell script calling us via D-Bus */
268 /* Launch the browser in the background -- our parent will
269 wait for it to claim the D-Bus name and then display the
270 window using D-Bus */
271 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
273 /* If we get here, exec() failed */
280 /* Open a MicroB window using the D-Bus interface
281 It's assumed that we have already released the D-Bus name and that it's been
282 ensured that MicroB has acquired com.nokia.osso_browser (otherwise this will
283 cause D-Bus to try forever to launch another browser-switchboard) */
285 #define LAUNCH_MICROB_BOOKMARK_WIN_OK 0x1
287 int launch_microb_open_window(struct swb_context *ctx, char *uri,
290 GError *gerror = NULL;
292 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
293 "com.nokia.osso_browser",
294 "/com/nokia/osso_browser/request",
295 "com.nokia.osso_browser");
297 log_msg("Couldn't get a com.nokia.osso_browser proxy\n");
301 if (!strcmp(uri, "new_window")) {
302 if (flags & LAUNCH_MICROB_BOOKMARK_WIN_OK) {
303 if (!dbus_g_proxy_call(g_proxy, "top_application",
304 &gerror, G_TYPE_INVALID,
306 log_msg("Opening window failed: %s\n",
308 g_error_free(gerror);
314 /* Since we can't detect when the bookmark window
315 closes, we'd have a corner case where, if the user
316 just closes the bookmark window without opening any
317 browser windows, we don't kill off MicroB or resume
318 handling com.nokia.osso_browser */
322 if (!dbus_g_proxy_call(g_proxy, "load_url",
327 log_msg("Opening window failed: %s\n", gerror->message);
328 g_error_free(gerror);
332 g_object_unref(g_proxy);
336 /* Launch Fremantle MicroB and kill it when the session is finished */
337 void launch_microb_fremantle_with_kill(struct swb_context *ctx, char *uri) {
340 char *homedir, *microb_profile_dir, *microb_lockfile;
343 DBusConnection *raw_connection;
346 struct inotify_event *event;
347 pid_t browserd_pid, waited_pid;
348 struct sigaction act, oldact;
351 /* Put together the path to the MicroB browserd lockfile */
352 if (!(homedir = getenv("HOME")))
353 homedir = DEFAULT_HOMEDIR;
354 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
355 if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
356 log_msg("calloc() failed\n");
359 snprintf(microb_profile_dir, len, "%s%s",
360 homedir, MICROB_PROFILE_DIR);
361 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
362 strlen("/") + strlen(MICROB_LOCKFILE) + 1;
363 if (!(microb_lockfile = calloc(len, sizeof(char)))) {
364 log_msg("calloc() failed\n");
367 snprintf(microb_lockfile, len, "%s%s/%s",
368 homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
370 /* Watch for the creation of a MicroB browserd lockfile
371 NB: The watch has to be set up here, before the browser
372 is launched, to make sure there's no race between browserd
373 starting and us creating the watch */
374 if ((fd = inotify_init()) == -1) {
375 log_perror(errno, "inotify_init");
378 if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
380 log_perror(errno, "inotify_add_watch");
383 free(microb_profile_dir);
385 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
386 acquiring the com.nokia.osso_browser D-Bus name. Again, this needs
387 to happen before the browser is launched, so that there's no race
388 between establishing the watch and browser startup. */
389 if (!(raw_connection = microb_start_dbus_watch_init())) {
393 /* Launch a MicroB browser process if it's not already running */
394 if ((pid = launch_microb_start_browser_process(raw_connection, fd)) < 0)
397 /* Release the osso_browser D-Bus name so that MicroB can take it */
398 dbus_release_osso_browser_name(ctx);
400 /* Wait for our child to start the browser UI process and
401 for it to acquire the com.nokia.osso_browser D-Bus name,
402 then make the appropriate method call to open the browser
404 microb_start_dbus_watch_wait(raw_connection);
405 microb_start_dbus_watch_remove(raw_connection);
406 if (!launch_microb_open_window(ctx, uri, 0)) {
410 /* Workaround: the browser process we started is going to want
411 to hang around forever, hogging the com.nokia.osso_browser
412 D-Bus interface while at it. To fix this, we notice that
413 when the last browser window closes, the browser UI restarts
414 its attached browserd process. Get the browserd process's
415 PID and use ptrace() to watch for process termination.
417 This has the problem of not being able to detect whether
418 the bookmark window is open and/or in use, but it's the best
419 that I can think of. Better suggestions would be greatly
423 /* If we didn't start the MicroB browser process ourselves, try
424 to get the PID of the browserd from the lockfile */
425 browserd_pid = get_browserd_pid(microb_lockfile);
429 /* If getting the lockfile PID failed, or the lockfile PID doesn't
430 exist, assume that we have a stale lockfile and wait for the new
431 browserd lockfile to be created */
432 if (browserd_pid <= 0 || kill(browserd_pid, 0) == ESRCH) {
433 log_msg("Waiting for browserd lockfile to be created\n");
434 memset(buf, '\0', 256);
435 /* read() blocks until there are events to be read */
436 while ((bytes_read = read(fd, buf, 255)) > 0) {
438 /* Loop until we see the event we're looking for
439 or until all the events are processed */
440 while (pos && (pos-buf) < bytes_read) {
441 event = (struct inotify_event *)pos;
442 len = sizeof(struct inotify_event) + event->len;
443 if (!strcmp(MICROB_LOCKFILE, event->name)) {
444 /* Lockfile created */
447 } else if ((pos-buf) + len < bytes_read)
448 /* More events to process */
451 /* All events processed */
455 /* Event found, stop looking */
457 memset(buf, '\0', 256);
460 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
461 if (browserd_pid == 0)
462 log_msg("Profile lockfile link lacks PID\n");
464 log_perror(-browserd_pid,
465 "readlink() on lockfile failed");
469 inotify_rm_watch(fd, inot_wd);
471 free(microb_lockfile);
473 /* Wait for the browserd to close */
474 log_msg("Waiting for MicroB (browserd pid %d) to finish\n",
476 /* Clear any existing SIGCHLD handler to prevent interference
478 act.sa_handler = SIG_DFL;
480 sigemptyset(&(act.sa_mask));
481 if (sigaction(SIGCHLD, &act, &oldact) == -1) {
482 log_perror(errno, "clearing SIGCHLD handler failed");
486 /* Trace the browserd to get a close notification */
488 if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
489 log_perror(errno, "PTRACE_ATTACH");
492 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
493 while ((waited_pid = wait(&status)) > 0) {
494 if (waited_pid != browserd_pid)
495 /* Not interested in other processes */
497 if (WIFEXITED(status) || WIFSIGNALED(status))
498 /* browserd exited */
500 else if (WIFSTOPPED(status)) {
501 /* browserd was sent a signal
502 We're responsible for making sure this signal gets
504 if (ignore_sigstop && WSTOPSIG(status) == SIGSTOP) {
505 /* Ignore the first SIGSTOP received
506 This is raised for some reason immediately
507 after we start tracing the process, and
508 won't be followed by a SIGCONT at any point
510 log_msg("Ignoring first SIGSTOP\n");
511 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
515 log_msg("Forwarding signal %d to browserd\n",
517 ptrace(PTRACE_CONT, browserd_pid, NULL,
522 /* Kill off browser UI
523 XXX: There is a race here with the restarting of the closed
524 browserd; if that happens before we kill the browser UI, the newly
525 started browserd may not close with the UI
526 XXX: Hope we don't cause data loss here! */
527 log_msg("Killing MicroB\n");
530 waitpid(pid, &status, 0);
532 system("kill `pidof browser` > /dev/null 2>&1");
535 /* Restore old SIGCHLD handler */
536 if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
537 log_perror(errno, "restoring old SIGCHLD handler failed");
541 dbus_request_osso_browser_name(ctx);
544 /* Launch a new window in Fremantle MicroB; don't kill the MicroB process
545 when the session is finished
546 This is designed to work with a prestarted MicroB process that runs
547 continuously in the background */
548 void launch_microb_fremantle(struct swb_context *ctx, char *uri) {
549 DBusConnection *raw_connection;
551 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
552 acquiring the com.nokia.osso_browser D-Bus name */
553 if (!(raw_connection = microb_start_dbus_watch_init())) {
557 /* Launch a MicroB browser process if it's not already running */
558 if (launch_microb_start_browser_process(raw_connection, -1) < 0)
561 /* Release the osso_browser D-Bus name so that MicroB can take it */
562 dbus_release_osso_browser_name(ctx);
564 /* Wait for MicroB to acquire com.nokia.osso_browser, then make the
565 appropriate method call to open the browser window. */
566 microb_start_dbus_watch_wait(raw_connection);
567 microb_start_dbus_watch_remove(raw_connection);
568 if (!launch_microb_open_window(ctx, uri,
569 LAUNCH_MICROB_BOOKMARK_WIN_OK)) {
573 /* Take back the osso_browser D-Bus name from MicroB */
574 dbus_request_osso_browser_name(ctx);
576 #endif /* FREMANTLE */
578 void launch_microb(struct swb_context *ctx, char *uri) {
579 int kill_browserd = 0;
588 log_msg("launch_microb with uri '%s'\n", uri);
590 /* Launch browserd if it's not running */
591 status = system("pidof browserd > /dev/null");
592 if (WIFEXITED(status) && WEXITSTATUS(status)) {
595 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
597 system("/usr/sbin/browserd -d > /dev/null 2>&1");
602 /* Do the insanity to launch Fremantle MicroB */
603 if ((ctx->default_browser_launcher == launch_microb &&
604 ctx->autostart_microb) || ctx->autostart_microb == 1) {
606 /* If MicroB is set as the default browser, or if the user has
607 configured MicroB to always be running, just send the
608 running MicroB the request */
609 launch_microb_fremantle(ctx, uri);
611 /* Otherwise, launch MicroB and kill it when the user's
612 MicroB session is done */
613 launch_microb_fremantle_with_kill(ctx, uri);
615 #else /* !FREMANTLE */
616 /* Release the osso_browser D-Bus name so that MicroB can take it */
617 dbus_release_osso_browser_name(ctx);
619 if ((pid = fork()) == -1) {
620 log_perror(errno, "fork");
626 waitpid(pid, &status, 0);
631 /* exec maemo-invoker directly instead of relying on the
632 /usr/bin/browser symlink, since /usr/bin/browser may have
633 been replaced with a shell script calling us via D-Bus */
634 if (!strcmp(uri, "new_window")) {
635 execl("/usr/bin/maemo-invoker",
636 "browser", (char *)NULL);
638 execl("/usr/bin/maemo-invoker",
639 "browser", "--url", uri, (char *)NULL);
643 dbus_request_osso_browser_name(ctx);
644 #endif /* FREMANTLE */
646 /* Kill off browserd if we started it */
648 system("kill `pidof browserd`");
650 if (!ctx || !ctx->continuous_mode)
654 static void launch_other_browser(struct swb_context *ctx, char *uri) {
656 char *quoted_uri, *quote;
658 size_t cmdlen, urilen;
659 size_t quoted_uri_size;
662 if (!uri || !strcmp(uri, "new_window"))
665 log_msg("launch_other_browser with uri '%s'\n", uri);
667 if ((urilen = strlen(uri)) > 0) {
668 /* Quote the URI to prevent the shell from interpreting it */
669 /* urilen+3 = length of URI + 2x \' + \0 */
670 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
672 snprintf(quoted_uri, urilen+3, "'%s'", uri);
674 /* If there are any 's in the original URI, URL-escape them
675 (replace them with %27) */
676 quoted_uri_size = urilen + 3;
677 quote = quoted_uri + 1;
678 while ((quote = strchr(quote, '\'')) &&
679 (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
680 /* Check to make sure we don't shrink the memory area
681 as a result of integer overflow */
682 if (quoted_uri_size+2 <= quoted_uri_size)
685 /* Grow the memory area;
686 2 = strlen("%27")-strlen("'") */
687 if (!(quoted_uri = realloc(quoted_uri,
690 quoted_uri_size = quoted_uri_size + 2;
692 /* Recalculate the location of the ' character --
693 realloc() may have moved the string in memory */
694 quote = quoted_uri + offset;
696 /* Move the string after the ', including the \0,
698 memmove(quote+3, quote+1, strlen(quote));
699 memcpy(quote, "%27", 3);
702 urilen = strlen(quoted_uri);
706 cmdlen = strlen(ctx->other_browser_cmd);
708 /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
709 replace "%s"), but is needed in the case other_browser_cmd has no %s
711 if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
713 snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
714 log_msg("command: '%s'\n", command);
716 if (ctx->continuous_mode) {
718 /* Parent process or error in fork() */
728 execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
732 /* The list of known browsers and how to launch them */
733 static struct browser_launcher browser_launchers[] = {
734 { "microb", launch_microb, NULL, NULL }, /* First entry is the default! */
735 { "tear", launch_tear, NULL, "/usr/bin/tear" },
736 { "fennec", NULL, "fennec %s", "/usr/bin/fennec" },
737 { "opera", NULL, "opera %s", "/usr/bin/opera" },
738 { "midori", NULL, "midori %s", "/usr/bin/midori" },
739 { NULL, NULL, NULL, NULL },
742 static void use_launcher_as_default(struct swb_context *ctx,
743 struct browser_launcher *browser) {
744 if (!ctx || !browser)
747 if (browser->launcher)
748 ctx->default_browser_launcher = browser->launcher;
749 else if (browser->other_browser_cmd) {
750 free(ctx->other_browser_cmd);
752 /* Make a copy of the string constant so that
753 ctx->other_browser_cmd is safe to free() */
754 ctx->other_browser_cmd = strdup(browser->other_browser_cmd);
755 if (!ctx->other_browser_cmd) {
756 log_msg("malloc failed!\n");
757 /* Ideally, we'd configure the built-in default here --
758 but it's possible we could be called in that path */
761 ctx->default_browser_launcher = launch_other_browser;
767 void update_default_browser(struct swb_context *ctx, char *default_browser) {
768 struct browser_launcher *browser;
773 /* Configure the built-in default to start -- that way, we can
774 handle errors by just returning */
775 use_launcher_as_default(ctx, &browser_launchers[0]);
777 if (!default_browser)
778 /* No default_browser configured -- use built-in default */
781 /* Go through the list of known browser launchers and use one if
783 for (browser = browser_launchers; browser->name; ++browser)
784 if (!strcmp(default_browser, browser->name)) {
785 /* Make sure the user's choice is installed on the
787 if (browser->binary && access(browser->binary, X_OK)) {
788 log_msg("%s appears not to be installed\n",
791 use_launcher_as_default(ctx, browser);
796 /* Deal with default_browser = "other" */
797 if (!strcmp(default_browser, "other")) {
798 if (ctx->other_browser_cmd)
799 ctx->default_browser_launcher = launch_other_browser;
801 log_msg("default_browser is 'other', but no other_browser_cmd set -- using default\n");
805 /* Unknown value of default_browser */
806 log_msg("Unknown default_browser %s, using default\n", default_browser);
810 void launch_browser(struct swb_context *ctx, char *uri) {
811 if (ctx && ctx->default_browser_launcher)
812 ctx->default_browser_launcher(ctx, uri);