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 /* Open a MicroB window using the D-Bus interface
242 It's assumed that we have already released the D-Bus name and that it's been
243 ensured that MicroB has acquired com.nokia.osso_browser (otherwise this will
244 cause D-Bus to try forever to launch another browser-switchboard) */
246 #define LAUNCH_MICROB_BOOKMARK_WIN_OK 0x1
248 int launch_microb_open_window(struct swb_context *ctx, char *uri,
251 GError *gerror = NULL;
253 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
254 "com.nokia.osso_browser",
255 "/com/nokia/osso_browser/request",
256 "com.nokia.osso_browser");
258 log_msg("Couldn't get a com.nokia.osso_browser proxy\n");
262 if (!strcmp(uri, "new_window")) {
263 if (flags & LAUNCH_MICROB_BOOKMARK_WIN_OK) {
264 if (!dbus_g_proxy_call(g_proxy, "top_application",
265 &gerror, G_TYPE_INVALID,
267 log_msg("Opening window failed: %s\n",
269 g_error_free(gerror);
275 /* Since we can't detect when the bookmark window
276 closes, we'd have a corner case where, if the user
277 just closes the bookmark window without opening any
278 browser windows, we don't kill off MicroB or resume
279 handling com.nokia.osso_browser */
283 if (!dbus_g_proxy_call(g_proxy, "load_url",
288 log_msg("Opening window failed: %s\n", gerror->message);
289 g_error_free(gerror);
293 g_object_unref(g_proxy);
297 /* Launch Fremantle MicroB and kill it when the session is finished */
298 void launch_microb_fremantle_with_kill(struct swb_context *ctx, char *uri) {
301 char *homedir, *microb_profile_dir, *microb_lockfile;
304 DBusConnection *raw_connection;
307 struct inotify_event *event;
308 pid_t browserd_pid, waited_pid;
309 struct sigaction act, oldact;
312 /* Put together the path to the MicroB browserd lockfile */
313 if (!(homedir = getenv("HOME")))
314 homedir = DEFAULT_HOMEDIR;
315 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
316 if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
317 log_msg("calloc() failed\n");
320 snprintf(microb_profile_dir, len, "%s%s",
321 homedir, MICROB_PROFILE_DIR);
322 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
323 strlen("/") + strlen(MICROB_LOCKFILE) + 1;
324 if (!(microb_lockfile = calloc(len, sizeof(char)))) {
325 log_msg("calloc() failed\n");
328 snprintf(microb_lockfile, len, "%s%s/%s",
329 homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
331 /* Watch for the creation of a MicroB browserd lockfile
332 NB: The watch has to be set up here, before the browser
333 is launched, to make sure there's no race between browserd
334 starting and us creating the watch */
335 if ((fd = inotify_init()) == -1) {
336 log_perror(errno, "inotify_init");
339 if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
341 log_perror(errno, "inotify_add_watch");
344 free(microb_profile_dir);
346 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
347 acquiring the com.nokia.osso_browser D-Bus name. Again, this needs
348 to happen before the browser is launched, so that there's no race
349 between establishing the watch and browser startup. */
350 if (!(raw_connection = microb_start_dbus_watch_init())) {
354 if ((pid = fork()) == -1) {
355 log_perror(errno, "fork");
361 dbus_connection_close(raw_connection);
362 dbus_connection_unref(raw_connection);
366 /* exec maemo-invoker directly instead of relying on the
367 /usr/bin/browser symlink, since /usr/bin/browser may have
368 been replaced with a shell script calling us via D-Bus */
369 /* Launch the browser in the background -- our parent will
370 wait for it to claim the D-Bus name and then display the
371 window using D-Bus */
372 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
374 /* If we get here, exec() failed */
378 /* Wait for our child to start the browser UI process and
379 for it to acquire the com.nokia.osso_browser D-Bus name,
380 then make the appropriate method call to open the browser
382 microb_start_dbus_watch_wait(raw_connection);
383 microb_start_dbus_watch_remove(raw_connection);
384 if (!launch_microb_open_window(ctx, uri, 0)) {
388 /* Workaround: the browser process we started is going to want
389 to hang around forever, hogging the com.nokia.osso_browser
390 D-Bus interface while at it. To fix this, we notice that
391 when the last browser window closes, the browser UI restarts
392 its attached browserd process. Get the browserd process's
393 PID and use ptrace() to watch for process termination.
395 This has the problem of not being able to detect whether
396 the bookmark window is open and/or in use, but it's the best
397 that I can think of. Better suggestions would be greatly
400 /* Wait for the MicroB browserd lockfile to be created */
401 log_msg("Waiting for browserd lockfile to be created\n");
402 memset(buf, '\0', 256);
403 /* read() blocks until there are events to be read */
404 while ((bytes_read = read(fd, buf, 255)) > 0) {
406 /* Loop until we see the event we're looking for
407 or until all the events are processed */
408 while (pos && (pos-buf) < bytes_read) {
409 event = (struct inotify_event *)pos;
410 len = sizeof(struct inotify_event) + event->len;
411 if (!strcmp(MICROB_LOCKFILE, event->name)) {
412 /* Lockfile created */
415 } else if ((pos-buf) + len < bytes_read)
416 /* More events to process */
419 /* All events processed */
423 /* Event found, stop looking */
425 memset(buf, '\0', 256);
427 inotify_rm_watch(fd, inot_wd);
430 /* Get the PID of the browserd from the lockfile */
431 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
432 if (browserd_pid == 0)
433 log_msg("Profile lockfile link lacks PID\n");
435 log_perror(-browserd_pid,
436 "readlink() on lockfile failed");
439 free(microb_lockfile);
441 /* Wait for the browserd to close */
442 log_msg("Waiting for MicroB (browserd pid %d) to finish\n",
444 /* Clear any existing SIGCHLD handler to prevent interference
446 act.sa_handler = SIG_DFL;
448 sigemptyset(&(act.sa_mask));
449 if (sigaction(SIGCHLD, &act, &oldact) == -1) {
450 log_perror(errno, "clearing SIGCHLD handler failed");
454 /* Trace the browserd to get a close notification */
456 if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
457 log_perror(errno, "PTRACE_ATTACH");
460 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
461 while ((waited_pid = wait(&status)) > 0) {
462 if (waited_pid != browserd_pid)
463 /* Not interested in other processes */
465 if (WIFEXITED(status) || WIFSIGNALED(status))
466 /* browserd exited */
468 else if (WIFSTOPPED(status)) {
469 /* browserd was sent a signal
470 We're responsible for making sure this signal gets
472 if (ignore_sigstop && WSTOPSIG(status) == SIGSTOP) {
473 /* Ignore the first SIGSTOP received
474 This is raised for some reason immediately
475 after we start tracing the process, and
476 won't be followed by a SIGCONT at any point
478 log_msg("Ignoring first SIGSTOP\n");
479 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
483 log_msg("Forwarding signal %d to browserd\n",
485 ptrace(PTRACE_CONT, browserd_pid, NULL,
490 /* Kill off browser UI
491 XXX: There is a race here with the restarting of the closed
492 browserd; if that happens before we kill the browser UI, the newly
493 started browserd may not close with the UI
494 XXX: Hope we don't cause data loss here! */
495 log_msg("Killing MicroB\n");
497 waitpid(pid, &status, 0);
499 /* Restore old SIGCHLD handler */
500 if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
501 log_perror(errno, "restoring old SIGCHLD handler failed");
505 #endif /* FREMANTLE */
507 void launch_microb(struct swb_context *ctx, char *uri) {
508 int kill_browserd = 0;
517 log_msg("launch_microb with uri '%s'\n", uri);
519 /* Launch browserd if it's not running */
520 status = system("pidof browserd > /dev/null");
521 if (WIFEXITED(status) && WEXITSTATUS(status)) {
524 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
526 system("/usr/sbin/browserd -d > /dev/null 2>&1");
530 /* Release the osso_browser D-Bus name so that MicroB can take it */
531 dbus_release_osso_browser_name(ctx);
534 /* Do the insanity to launch Fremantle MicroB */
535 launch_microb_fremantle_with_kill(ctx, uri);
536 #else /* !FREMANTLE */
537 if ((pid = fork()) == -1) {
538 log_perror(errno, "fork");
544 waitpid(pid, &status, 0);
549 /* exec maemo-invoker directly instead of relying on the
550 /usr/bin/browser symlink, since /usr/bin/browser may have
551 been replaced with a shell script calling us via D-Bus */
552 if (!strcmp(uri, "new_window")) {
553 execl("/usr/bin/maemo-invoker",
554 "browser", (char *)NULL);
556 execl("/usr/bin/maemo-invoker",
557 "browser", "--url", uri, (char *)NULL);
560 #endif /* FREMANTLE */
562 /* Kill off browserd if we started it */
564 system("kill `pidof browserd`");
566 if (!ctx || !ctx->continuous_mode)
569 dbus_request_osso_browser_name(ctx);
572 static void launch_other_browser(struct swb_context *ctx, char *uri) {
574 char *quoted_uri, *quote;
576 size_t cmdlen, urilen;
577 size_t quoted_uri_size;
580 if (!uri || !strcmp(uri, "new_window"))
583 log_msg("launch_other_browser with uri '%s'\n", uri);
585 if ((urilen = strlen(uri)) > 0) {
586 /* Quote the URI to prevent the shell from interpreting it */
587 /* urilen+3 = length of URI + 2x \' + \0 */
588 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
590 snprintf(quoted_uri, urilen+3, "'%s'", uri);
592 /* If there are any 's in the original URI, URL-escape them
593 (replace them with %27) */
594 quoted_uri_size = urilen + 3;
595 quote = quoted_uri + 1;
596 while ((quote = strchr(quote, '\'')) &&
597 (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
598 /* Check to make sure we don't shrink the memory area
599 as a result of integer overflow */
600 if (quoted_uri_size+2 <= quoted_uri_size)
603 /* Grow the memory area;
604 2 = strlen("%27")-strlen("'") */
605 if (!(quoted_uri = realloc(quoted_uri,
608 quoted_uri_size = quoted_uri_size + 2;
610 /* Recalculate the location of the ' character --
611 realloc() may have moved the string in memory */
612 quote = quoted_uri + offset;
614 /* Move the string after the ', including the \0,
616 memmove(quote+3, quote+1, strlen(quote));
617 memcpy(quote, "%27", 3);
620 urilen = strlen(quoted_uri);
624 cmdlen = strlen(ctx->other_browser_cmd);
626 /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
627 replace "%s"), but is needed in the case other_browser_cmd has no %s
629 if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
631 snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
632 log_msg("command: '%s'\n", command);
634 if (ctx->continuous_mode) {
636 /* Parent process or error in fork() */
646 execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
650 /* The list of known browsers and how to launch them */
651 static struct browser_launcher browser_launchers[] = {
652 { "microb", launch_microb, NULL, NULL }, /* First entry is the default! */
653 { "tear", launch_tear, NULL, "/usr/bin/tear" },
654 { "fennec", NULL, "fennec %s", "/usr/bin/fennec" },
655 { "opera", NULL, "opera %s", "/usr/bin/opera" },
656 { "midori", NULL, "midori %s", "/usr/bin/midori" },
657 { NULL, NULL, NULL, NULL },
660 static void use_launcher_as_default(struct swb_context *ctx,
661 struct browser_launcher *browser) {
662 if (!ctx || !browser)
665 if (browser->launcher)
666 ctx->default_browser_launcher = browser->launcher;
667 else if (browser->other_browser_cmd) {
668 free(ctx->other_browser_cmd);
670 /* Make a copy of the string constant so that
671 ctx->other_browser_cmd is safe to free() */
672 ctx->other_browser_cmd = strdup(browser->other_browser_cmd);
673 if (!ctx->other_browser_cmd) {
674 log_msg("malloc failed!\n");
675 /* Ideally, we'd configure the built-in default here --
676 but it's possible we could be called in that path */
679 ctx->default_browser_launcher = launch_other_browser;
685 void update_default_browser(struct swb_context *ctx, char *default_browser) {
686 struct browser_launcher *browser;
691 /* Configure the built-in default to start -- that way, we can
692 handle errors by just returning */
693 use_launcher_as_default(ctx, &browser_launchers[0]);
695 if (!default_browser)
696 /* No default_browser configured -- use built-in default */
699 /* Go through the list of known browser launchers and use one if
701 for (browser = browser_launchers; browser->name; ++browser)
702 if (!strcmp(default_browser, browser->name)) {
703 /* Make sure the user's choice is installed on the
705 if (browser->binary && access(browser->binary, X_OK)) {
706 log_msg("%s appears not to be installed\n",
709 use_launcher_as_default(ctx, browser);
714 /* Deal with default_browser = "other" */
715 if (!strcmp(default_browser, "other")) {
716 if (ctx->other_browser_cmd)
717 ctx->default_browser_launcher = launch_other_browser;
719 log_msg("default_browser is 'other', but no other_browser_cmd set -- using default\n");
723 /* Unknown value of default_browser */
724 log_msg("Unknown default_browser %s, using default\n", default_browser);
728 void launch_browser(struct swb_context *ctx, char *uri) {
729 if (ctx && ctx->default_browser_launcher)
730 ctx->default_browser_launcher(ctx, uri);