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;
57 static int microb_started = 0;
59 /* Check to see whether MicroB is ready to handle D-Bus requests yet
60 See the comments in launch_microb to understand how this works. */
61 static DBusHandlerResult check_microb_started(DBusConnection *connection,
65 char *name, *old, *new;
67 log_msg("Checking to see if MicroB is ready\n");
68 dbus_error_init(&error);
69 if (!dbus_message_get_args(message, &error,
70 DBUS_TYPE_STRING, &name,
71 DBUS_TYPE_STRING, &old,
72 DBUS_TYPE_STRING, &new,
74 log_msg("%s\n", error.message);
75 dbus_error_free(&error);
76 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
78 /* If old is an empty string, then the name has been acquired, and
79 MicroB should be ready to handle our request */
80 if (strlen(old) == 0) {
81 log_msg("MicroB ready\n");
85 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
88 /* Get a browserd PID from the corresponding Mozilla profile lockfile */
89 static pid_t get_browserd_pid(const char *lockfile) {
92 /* The lockfile is a symlink pointing to "[ipaddr]:+[pid]", so read in
93 the target of the symlink and parse it that way */
94 memset(buf, '\0', 256);
95 if (readlink(lockfile, buf, 255) == -1)
97 if (!(tmp = strstr(buf, ":+")))
99 tmp += 2; /* Skip over the ":+" */
105 /* Close stdin/stdout/stderr and replace with /dev/null */
106 static int close_stdio(void) {
109 if ((fd = open("/dev/null", O_RDWR)) == -1)
112 if (dup2(fd, 0) == -1 || dup2(fd, 1) == -1 || dup2(fd, 2) == -1)
119 static void launch_tear(struct swb_context *ctx, char *uri) {
121 static DBusGProxy *tear_proxy = NULL;
122 GError *error = NULL;
128 log_msg("launch_tear with uri '%s'\n", uri);
130 /* We should be able to just call the D-Bus service to open Tear ...
131 but if Tear's not open, that cuases D-Bus to start Tear and then
132 pass it the OpenAddress call, which results in two browser windows.
133 Properly fixing this probably requires Tear to provide a D-Bus
134 method that opens an address in an existing window, but for now work
135 around by just invoking Tear with exec() if it's not running. */
136 status = system("pidof tear > /dev/null");
137 if (WIFEXITED(status) && !WEXITSTATUS(status)) {
139 if (!(tear_proxy = dbus_g_proxy_new_for_name(
143 "com.nokia.Tear"))) {
144 log_msg("Failed to create proxy for com.nokia.Tear D-Bus interface\n");
149 if (!dbus_g_proxy_call(tear_proxy, "OpenAddress", &error,
150 G_TYPE_STRING, uri, G_TYPE_INVALID,
152 log_msg("Opening window failed: %s\n", error->message);
155 if (!ctx->continuous_mode)
158 if (ctx->continuous_mode) {
159 if ((pid = fork()) != 0) {
160 /* Parent process or error in fork() */
161 log_msg("child: %d\n", (int)pid);
168 execl("/usr/bin/tear", "/usr/bin/tear", uri, (char *)NULL);
172 void launch_microb(struct swb_context *ctx, char *uri) {
173 int kill_browserd = 0;
177 char *homedir, *microb_profile_dir, *microb_lockfile;
180 DBusConnection *raw_connection;
181 DBusError dbus_error;
182 DBusHandleMessageFunction filter_func;
184 GError *gerror = NULL;
187 struct inotify_event *event;
188 pid_t browserd_pid, waited_pid;
189 struct sigaction act, oldact;
196 log_msg("launch_microb with uri '%s'\n", uri);
198 /* Launch browserd if it's not running */
199 status = system("pidof browserd > /dev/null");
200 if (WIFEXITED(status) && WEXITSTATUS(status)) {
203 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
205 system("/usr/sbin/browserd -d > /dev/null 2>&1");
209 /* Release the osso_browser D-Bus name so that MicroB can take it */
210 dbus_release_osso_browser_name(ctx);
213 /* Put together the path to the MicroB browserd lockfile */
214 if (!(homedir = getenv("HOME")))
215 homedir = DEFAULT_HOMEDIR;
216 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
217 if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
218 log_msg("calloc() failed\n");
221 snprintf(microb_profile_dir, len, "%s%s",
222 homedir, MICROB_PROFILE_DIR);
223 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
224 strlen("/") + strlen(MICROB_LOCKFILE) + 1;
225 if (!(microb_lockfile = calloc(len, sizeof(char)))) {
226 log_msg("calloc() failed\n");
229 snprintf(microb_lockfile, len, "%s%s/%s",
230 homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
232 /* Watch for the creation of a MicroB browserd lockfile
233 NB: The watch has to be set up here, before the browser
234 is launched, to make sure there's no race between browserd
235 starting and us creating the watch */
236 if ((fd = inotify_init()) == -1) {
237 log_perror(errno, "inotify_init");
240 if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
242 log_perror(errno, "inotify_add_watch");
245 free(microb_profile_dir);
247 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
248 acquiring the com.nokia.osso_browser D-Bus name. Again, this needs
249 to happen before the browser is launched, so that there's no race
250 between establishing the watch and browser startup.
252 Ideas for how to do this monitoring derived from the dbus-monitor
253 code (tools/dbus-monitor.c in the D-Bus codebase). */
254 dbus_error_init(&dbus_error);
256 raw_connection = dbus_bus_get_private(DBUS_BUS_SESSION, &dbus_error);
257 if (!raw_connection) {
258 log_msg("Failed to open connection to session bus: %s\n",
260 dbus_error_free(&dbus_error);
264 dbus_bus_add_match(raw_connection,
265 "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
267 if (dbus_error_is_set(&dbus_error)) {
268 log_msg("Failed to set up watch for browser UI start: %s\n",
270 dbus_error_free(&dbus_error);
273 filter_func = check_microb_started;
274 if (!dbus_connection_add_filter(raw_connection,
275 filter_func, NULL, NULL)) {
276 log_msg("Failed to set up watch filter!\n");
280 if ((pid = fork()) == -1) {
281 log_perror(errno, "fork");
287 /* Wait for our child to start the browser UI process and
288 for it to acquire the com.nokia.osso_browser D-Bus name,
289 then make the appropriate method call to open the browser
292 log_msg("Waiting for MicroB to start\n");
293 while (!microb_started &&
294 dbus_connection_read_write_dispatch(raw_connection,
296 dbus_connection_remove_filter(raw_connection,
298 dbus_bus_remove_match(raw_connection,
299 "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
301 if (dbus_error_is_set(&dbus_error))
302 /* Don't really care -- about to disconnect from the
304 dbus_error_free(&dbus_error);
305 dbus_connection_close(raw_connection);
306 dbus_connection_unref(raw_connection);
308 /* Browser UI's started, send it the request for a new window
310 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
311 "com.nokia.osso_browser",
312 "/com/nokia/osso_browser/request",
313 "com.nokia.osso_browser");
315 log_msg("Couldn't get a com.nokia.osso_browser proxy\n");
318 if (!strcmp(uri, "new_window")) {
319 #if 0 /* Since we can't detect when the bookmark window closes, we'd have a
320 corner case where, if the user just closes the bookmark window
321 without opening any browser windows, we don't kill off MicroB or
322 resume handling com.nokia.osso_browser */
323 if (!dbus_g_proxy_call(g_proxy, "top_application",
324 &gerror, G_TYPE_INVALID,
326 log_msg("Opening window failed: %s\n",
331 if (!dbus_g_proxy_call(g_proxy, "load_url",
333 G_TYPE_STRING, "about:blank",
336 log_msg("Opening window failed: %s\n",
341 if (!dbus_g_proxy_call(g_proxy, "load_url",
346 log_msg("Opening window failed: %s\n",
351 g_object_unref(g_proxy);
353 /* Workaround: the browser process we started is going to want
354 to hang around forever, hogging the com.nokia.osso_browser
355 D-Bus interface while at it. To fix this, we notice that
356 when the last browser window closes, the browser UI restarts
357 its attached browserd process. Get the browserd process's
358 PID and use ptrace() to watch for process termination.
360 This has the problem of not being able to detect whether
361 the bookmark window is open and/or in use, but it's the best
362 that I can think of. Better suggestions would be greatly
365 /* Wait for the MicroB browserd lockfile to be created */
366 log_msg("Waiting for browserd lockfile to be created\n");
367 memset(buf, '\0', 256);
368 /* read() blocks until there are events to be read */
369 while ((bytes_read = read(fd, buf, 255)) > 0) {
371 /* Loop until we see the event we're looking for
372 or until all the events are processed */
373 while (pos && (pos-buf) < bytes_read) {
374 event = (struct inotify_event *)pos;
375 len = sizeof(struct inotify_event)
377 if (!strcmp(MICROB_LOCKFILE,
379 /* Lockfile created */
382 } else if ((pos-buf) + len < bytes_read)
383 /* More events to process */
386 /* All events processed */
390 /* Event found, stop looking */
392 memset(buf, '\0', 256);
394 inotify_rm_watch(fd, inot_wd);
397 /* Get the PID of the browserd from the lockfile */
398 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
399 if (browserd_pid == 0)
400 log_msg("Profile lockfile link lacks PID\n");
402 log_perror(-browserd_pid,
403 "readlink() on lockfile failed");
406 free(microb_lockfile);
408 /* Wait for the browserd to close */
409 log_msg("Waiting for MicroB (browserd pid %d) to finish\n",
411 /* Clear any existing SIGCHLD handler to prevent interference
413 act.sa_handler = SIG_DFL;
415 sigemptyset(&(act.sa_mask));
416 if (sigaction(SIGCHLD, &act, &oldact) == -1) {
417 log_perror(errno, "clearing SIGCHLD handler failed");
421 /* Trace the browserd to get a close notification */
423 if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
424 log_perror(errno, "PTRACE_ATTACH");
427 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
428 while ((waited_pid = wait(&status)) > 0) {
429 if (waited_pid != browserd_pid)
430 /* Not interested in other processes */
432 if (WIFEXITED(status) || WIFSIGNALED(status))
433 /* browserd exited */
435 else if (WIFSTOPPED(status)) {
436 /* browserd was sent a signal
437 We're responsible for making sure this
438 signal gets delivered */
439 if (ignore_sigstop &&
440 WSTOPSIG(status) == SIGSTOP) {
441 /* Ignore the first SIGSTOP received
442 This is raised for some reason
443 immediately after we start tracing
444 the process, and won't be followed
445 by a SIGCONT at any point */
446 log_msg("Ignoring first SIGSTOP\n");
447 ptrace(PTRACE_CONT, browserd_pid,
452 log_msg("Forwarding signal %d to browserd\n",
454 ptrace(PTRACE_CONT, browserd_pid,
455 NULL, WSTOPSIG(status));
459 /* Kill off browser UI
460 XXX: There is a race here with the restarting of the closed
461 browserd; if that happens before we kill the browser UI, the
462 newly started browserd may not close with the UI
463 XXX: Hope we don't cause data loss here! */
464 log_msg("Killing MicroB\n");
466 waitpid(pid, &status, 0);
468 /* Restore old SIGCHLD handler */
469 if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
471 "restoring old SIGCHLD handler failed");
476 dbus_connection_close(raw_connection);
477 dbus_connection_unref(raw_connection);
481 /* exec maemo-invoker directly instead of relying on the
482 /usr/bin/browser symlink, since /usr/bin/browser may have
483 been replaced with a shell script calling us via D-Bus */
484 /* Launch the browser in the background -- our parent will
485 wait for it to claim the D-Bus name and then display the
486 window using D-Bus */
487 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
489 #else /* !FREMANTLE */
490 if ((pid = fork()) == -1) {
491 log_perror(errno, "fork");
497 waitpid(pid, &status, 0);
502 /* exec maemo-invoker directly instead of relying on the
503 /usr/bin/browser symlink, since /usr/bin/browser may have
504 been replaced with a shell script calling us via D-Bus */
505 if (!strcmp(uri, "new_window")) {
506 execl("/usr/bin/maemo-invoker",
507 "browser", (char *)NULL);
509 execl("/usr/bin/maemo-invoker",
510 "browser", "--url", uri, (char *)NULL);
513 #endif /* FREMANTLE */
515 /* Kill off browserd if we started it */
517 system("kill `pidof browserd`");
519 if (!ctx || !ctx->continuous_mode)
522 dbus_request_osso_browser_name(ctx);
525 static void launch_other_browser(struct swb_context *ctx, char *uri) {
527 char *quoted_uri, *quote;
529 size_t cmdlen, urilen;
530 size_t quoted_uri_size;
533 if (!uri || !strcmp(uri, "new_window"))
536 log_msg("launch_other_browser with uri '%s'\n", uri);
538 if ((urilen = strlen(uri)) > 0) {
539 /* Quote the URI to prevent the shell from interpreting it */
540 /* urilen+3 = length of URI + 2x \' + \0 */
541 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
543 snprintf(quoted_uri, urilen+3, "'%s'", uri);
545 /* If there are any 's in the original URI, URL-escape them
546 (replace them with %27) */
547 quoted_uri_size = urilen + 3;
548 quote = quoted_uri + 1;
549 while ((quote = strchr(quote, '\'')) &&
550 (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
551 /* Check to make sure we don't shrink the memory area
552 as a result of integer overflow */
553 if (quoted_uri_size+2 <= quoted_uri_size)
556 /* Grow the memory area;
557 2 = strlen("%27")-strlen("'") */
558 if (!(quoted_uri = realloc(quoted_uri,
561 quoted_uri_size = quoted_uri_size + 2;
563 /* Recalculate the location of the ' character --
564 realloc() may have moved the string in memory */
565 quote = quoted_uri + offset;
567 /* Move the string after the ', including the \0,
569 memmove(quote+3, quote+1, strlen(quote));
570 memcpy(quote, "%27", 3);
573 urilen = strlen(quoted_uri);
577 cmdlen = strlen(ctx->other_browser_cmd);
579 /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
580 replace "%s"), but is needed in the case other_browser_cmd has no %s
582 if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
584 snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
585 log_msg("command: '%s'\n", command);
587 if (ctx->continuous_mode) {
589 /* Parent process or error in fork() */
599 execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
603 /* The list of known browsers and how to launch them */
604 static struct browser_launcher browser_launchers[] = {
605 { "microb", launch_microb, NULL }, /* First entry is the default! */
606 { "tear", launch_tear, NULL },
607 { "fennec", NULL, "fennec %s" },
608 { "opera", NULL, "opera %s" },
609 { "midori", NULL, "midori %s" },
610 { NULL, NULL, NULL },
613 static void use_launcher_as_default(struct swb_context *ctx,
614 struct browser_launcher *browser) {
615 if (!ctx || !browser)
618 if (browser->launcher)
619 ctx->default_browser_launcher = browser->launcher;
620 else if (browser->other_browser_cmd) {
621 free(ctx->other_browser_cmd);
623 /* Make a copy of the string constant so that
624 ctx->other_browser_cmd is safe to free() */
625 ctx->other_browser_cmd = strdup(browser->other_browser_cmd);
626 if (!ctx->other_browser_cmd) {
627 log_msg("malloc failed!\n");
628 /* Ideally, we'd configure the built-in default here --
629 but it's possible we could be called in that path */
632 ctx->default_browser_launcher = launch_other_browser;
638 void update_default_browser(struct swb_context *ctx, char *default_browser) {
639 struct browser_launcher *browser;
644 /* Configure the built-in default to start -- that way, we can
645 handle errors by just returning */
646 use_launcher_as_default(ctx, &browser_launchers[0]);
648 if (!default_browser)
649 /* No default_browser configured -- use built-in default */
652 /* Go through the list of known browser launchers and use one if
654 for (browser = browser_launchers; browser->name; ++browser)
655 if (!strcmp(default_browser, browser->name)) {
656 use_launcher_as_default(ctx, browser);
660 /* Deal with default_browser = "other" */
661 if (!strcmp(default_browser, "other")) {
662 if (ctx->other_browser_cmd)
663 ctx->default_browser_launcher = launch_other_browser;
665 log_msg("default_browser is 'other', but no other_browser_cmd set -- using default\n");
669 /* Unknown value of default_browser */
670 log_msg("Unknown default_browser %s, using default\n", default_browser);
674 void launch_browser(struct swb_context *ctx, char *uri) {
675 if (ctx && ctx->default_browser_launcher)
676 ctx->default_browser_launcher(ctx, uri);