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 #define LAUNCH_DEFAULT_BROWSER launch_microb
53 static int microb_started = 0;
55 /* Check to see whether MicroB is ready to handle D-Bus requests yet
56 See the comments in launch_microb to understand how this works. */
57 static DBusHandlerResult check_microb_started(DBusConnection *connection,
61 char *name, *old, *new;
63 log_msg("Checking to see if MicroB is ready\n");
64 dbus_error_init(&error);
65 if (!dbus_message_get_args(message, &error,
66 DBUS_TYPE_STRING, &name,
67 DBUS_TYPE_STRING, &old,
68 DBUS_TYPE_STRING, &new,
70 log_msg("%s\n", error.message);
71 dbus_error_free(&error);
72 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
74 /* If old is an empty string, then the name has been acquired, and
75 MicroB should be ready to handle our request */
76 if (strlen(old) == 0) {
77 log_msg("MicroB ready\n");
81 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
84 /* Get a browserd PID from the corresponding Mozilla profile lockfile */
85 static pid_t get_browserd_pid(const char *lockfile) {
88 /* The lockfile is a symlink pointing to "[ipaddr]:+[pid]", so read in
89 the target of the symlink and parse it that way */
90 memset(buf, '\0', 256);
91 if (readlink(lockfile, buf, 255) == -1)
93 if (!(tmp = strstr(buf, ":+")))
95 tmp += 2; /* Skip over the ":+" */
101 /* Close stdin/stdout/stderr and replace with /dev/null */
102 static int close_stdio(void) {
105 if ((fd = open("/dev/null", O_RDWR)) == -1)
108 if (dup2(fd, 0) == -1 || dup2(fd, 1) == -1 || dup2(fd, 2) == -1)
115 static void launch_tear(struct swb_context *ctx, char *uri) {
117 static DBusGProxy *tear_proxy = NULL;
118 GError *error = NULL;
124 log_msg("launch_tear with uri '%s'\n", uri);
126 /* We should be able to just call the D-Bus service to open Tear ...
127 but if Tear's not open, that cuases D-Bus to start Tear and then
128 pass it the OpenAddress call, which results in two browser windows.
129 Properly fixing this probably requires Tear to provide a D-Bus
130 method that opens an address in an existing window, but for now work
131 around by just invoking Tear with exec() if it's not running. */
132 status = system("pidof tear > /dev/null");
133 if (WIFEXITED(status) && !WEXITSTATUS(status)) {
135 if (!(tear_proxy = dbus_g_proxy_new_for_name(
139 "com.nokia.Tear"))) {
140 log_msg("Failed to create proxy for com.nokia.Tear D-Bus interface\n");
145 if (!dbus_g_proxy_call(tear_proxy, "OpenAddress", &error,
146 G_TYPE_STRING, uri, G_TYPE_INVALID,
148 log_msg("Opening window failed: %s\n", error->message);
151 if (!ctx->continuous_mode)
154 if (ctx->continuous_mode) {
155 if ((pid = fork()) != 0) {
156 /* Parent process or error in fork() */
157 log_msg("child: %d\n", (int)pid);
164 execl("/usr/bin/tear", "/usr/bin/tear", uri, (char *)NULL);
168 void launch_microb(struct swb_context *ctx, char *uri) {
169 int kill_browserd = 0;
173 char *homedir, *microb_profile_dir, *microb_lockfile;
176 DBusConnection *raw_connection;
177 DBusError dbus_error;
178 DBusHandleMessageFunction filter_func;
180 GError *gerror = NULL;
183 struct inotify_event *event;
184 pid_t browserd_pid, waited_pid;
185 struct sigaction act, oldact;
192 log_msg("launch_microb with uri '%s'\n", uri);
194 /* Launch browserd if it's not running */
195 status = system("pidof browserd > /dev/null");
196 if (WIFEXITED(status) && WEXITSTATUS(status)) {
199 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
201 system("/usr/sbin/browserd -d > /dev/null 2>&1");
205 /* Release the osso_browser D-Bus name so that MicroB can take it */
206 dbus_release_osso_browser_name(ctx);
209 /* Put together the path to the MicroB browserd lockfile */
210 if (!(homedir = getenv("HOME")))
211 homedir = DEFAULT_HOMEDIR;
212 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
213 if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
214 log_msg("calloc() failed\n");
217 snprintf(microb_profile_dir, len, "%s%s",
218 homedir, MICROB_PROFILE_DIR);
219 len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
220 strlen("/") + strlen(MICROB_LOCKFILE) + 1;
221 if (!(microb_lockfile = calloc(len, sizeof(char)))) {
222 log_msg("calloc() failed\n");
225 snprintf(microb_lockfile, len, "%s%s/%s",
226 homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
228 /* Watch for the creation of a MicroB browserd lockfile
229 NB: The watch has to be set up here, before the browser
230 is launched, to make sure there's no race between browserd
231 starting and us creating the watch */
232 if ((fd = inotify_init()) == -1) {
233 log_perror(errno, "inotify_init");
236 if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
238 log_perror(errno, "inotify_add_watch");
241 free(microb_profile_dir);
243 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
244 acquiring the com.nokia.osso_browser D-Bus name. Again, this needs
245 to happen before the browser is launched, so that there's no race
246 between establishing the watch and browser startup.
248 Ideas for how to do this monitoring derived from the dbus-monitor
249 code (tools/dbus-monitor.c in the D-Bus codebase). */
250 dbus_error_init(&dbus_error);
252 raw_connection = dbus_bus_get_private(DBUS_BUS_SESSION, &dbus_error);
253 if (!raw_connection) {
254 log_msg("Failed to open connection to session bus: %s\n",
256 dbus_error_free(&dbus_error);
260 dbus_bus_add_match(raw_connection,
261 "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
263 if (dbus_error_is_set(&dbus_error)) {
264 log_msg("Failed to set up watch for browser UI start: %s\n",
266 dbus_error_free(&dbus_error);
269 filter_func = check_microb_started;
270 if (!dbus_connection_add_filter(raw_connection,
271 filter_func, NULL, NULL)) {
272 log_msg("Failed to set up watch filter!\n");
276 if ((pid = fork()) == -1) {
277 log_perror(errno, "fork");
283 /* Wait for our child to start the browser UI process and
284 for it to acquire the com.nokia.osso_browser D-Bus name,
285 then make the appropriate method call to open the browser
288 log_msg("Waiting for MicroB to start\n");
289 while (!microb_started &&
290 dbus_connection_read_write_dispatch(raw_connection,
292 dbus_connection_remove_filter(raw_connection,
294 dbus_bus_remove_match(raw_connection,
295 "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
297 if (dbus_error_is_set(&dbus_error))
298 /* Don't really care -- about to disconnect from the
300 dbus_error_free(&dbus_error);
301 dbus_connection_close(raw_connection);
302 dbus_connection_unref(raw_connection);
304 /* Browser UI's started, send it the request for a new window
306 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
307 "com.nokia.osso_browser",
308 "/com/nokia/osso_browser/request",
309 "com.nokia.osso_browser");
311 log_msg("Couldn't get a com.nokia.osso_browser proxy\n");
314 if (!strcmp(uri, "new_window")) {
315 #if 0 /* Since we can't detect when the bookmark window closes, we'd have a
316 corner case where, if the user just closes the bookmark window
317 without opening any browser windows, we don't kill off MicroB or
318 resume handling com.nokia.osso_browser */
319 if (!dbus_g_proxy_call(g_proxy, "top_application",
320 &gerror, G_TYPE_INVALID,
322 log_msg("Opening window failed: %s\n",
327 if (!dbus_g_proxy_call(g_proxy, "load_url",
329 G_TYPE_STRING, "about:blank",
332 log_msg("Opening window failed: %s\n",
337 if (!dbus_g_proxy_call(g_proxy, "load_url",
342 log_msg("Opening window failed: %s\n",
347 g_object_unref(g_proxy);
349 /* Workaround: the browser process we started is going to want
350 to hang around forever, hogging the com.nokia.osso_browser
351 D-Bus interface while at it. To fix this, we notice that
352 when the last browser window closes, the browser UI restarts
353 its attached browserd process. Get the browserd process's
354 PID and use ptrace() to watch for process termination.
356 This has the problem of not being able to detect whether
357 the bookmark window is open and/or in use, but it's the best
358 that I can think of. Better suggestions would be greatly
361 /* Wait for the MicroB browserd lockfile to be created */
362 log_msg("Waiting for browserd lockfile to be created\n");
363 memset(buf, '\0', 256);
364 /* read() blocks until there are events to be read */
365 while ((bytes_read = read(fd, buf, 255)) > 0) {
367 /* Loop until we see the event we're looking for
368 or until all the events are processed */
369 while (pos && (pos-buf) < bytes_read) {
370 event = (struct inotify_event *)pos;
371 len = sizeof(struct inotify_event)
373 if (!strcmp(MICROB_LOCKFILE,
375 /* Lockfile created */
378 } else if ((pos-buf) + len < bytes_read)
379 /* More events to process */
382 /* All events processed */
386 /* Event found, stop looking */
388 memset(buf, '\0', 256);
390 inotify_rm_watch(fd, inot_wd);
393 /* Get the PID of the browserd from the lockfile */
394 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
395 if (browserd_pid == 0)
396 log_msg("Profile lockfile link lacks PID\n");
398 log_perror(-browserd_pid,
399 "readlink() on lockfile failed");
402 free(microb_lockfile);
404 /* Wait for the browserd to close */
405 log_msg("Waiting for MicroB (browserd pid %d) to finish\n",
407 /* Clear any existing SIGCHLD handler to prevent interference
409 act.sa_handler = SIG_DFL;
411 sigemptyset(&(act.sa_mask));
412 if (sigaction(SIGCHLD, &act, &oldact) == -1) {
413 log_perror(errno, "clearing SIGCHLD handler failed");
417 /* Trace the browserd to get a close notification */
419 if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
420 log_perror(errno, "PTRACE_ATTACH");
423 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
424 while ((waited_pid = wait(&status)) > 0) {
425 if (waited_pid != browserd_pid)
426 /* Not interested in other processes */
428 if (WIFEXITED(status) || WIFSIGNALED(status))
429 /* browserd exited */
431 else if (WIFSTOPPED(status)) {
432 /* browserd was sent a signal
433 We're responsible for making sure this
434 signal gets delivered */
435 if (ignore_sigstop &&
436 WSTOPSIG(status) == SIGSTOP) {
437 /* Ignore the first SIGSTOP received
438 This is raised for some reason
439 immediately after we start tracing
440 the process, and won't be followed
441 by a SIGCONT at any point */
442 log_msg("Ignoring first SIGSTOP\n");
443 ptrace(PTRACE_CONT, browserd_pid,
448 log_msg("Forwarding signal %d to browserd\n",
450 ptrace(PTRACE_CONT, browserd_pid,
451 NULL, WSTOPSIG(status));
455 /* Kill off browser UI
456 XXX: There is a race here with the restarting of the closed
457 browserd; if that happens before we kill the browser UI, the
458 newly started browserd may not close with the UI
459 XXX: Hope we don't cause data loss here! */
460 log_msg("Killing MicroB\n");
462 waitpid(pid, &status, 0);
464 /* Restore old SIGCHLD handler */
465 if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
467 "restoring old SIGCHLD handler failed");
472 dbus_connection_close(raw_connection);
473 dbus_connection_unref(raw_connection);
477 /* exec maemo-invoker directly instead of relying on the
478 /usr/bin/browser symlink, since /usr/bin/browser may have
479 been replaced with a shell script calling us via D-Bus */
480 /* Launch the browser in the background -- our parent will
481 wait for it to claim the D-Bus name and then display the
482 window using D-Bus */
483 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
485 #else /* !FREMANTLE */
486 if ((pid = fork()) == -1) {
487 log_perror(errno, "fork");
493 waitpid(pid, &status, 0);
498 /* exec maemo-invoker directly instead of relying on the
499 /usr/bin/browser symlink, since /usr/bin/browser may have
500 been replaced with a shell script calling us via D-Bus */
501 if (!strcmp(uri, "new_window")) {
502 execl("/usr/bin/maemo-invoker",
503 "browser", (char *)NULL);
505 execl("/usr/bin/maemo-invoker",
506 "browser", "--url", uri, (char *)NULL);
509 #endif /* FREMANTLE */
511 /* Kill off browserd if we started it */
513 system("kill `pidof browserd`");
515 if (!ctx || !ctx->continuous_mode)
518 dbus_request_osso_browser_name(ctx);
521 static void launch_other_browser(struct swb_context *ctx, char *uri) {
523 char *quoted_uri, *quote;
525 size_t cmdlen, urilen;
526 size_t quoted_uri_size;
529 if (!uri || !strcmp(uri, "new_window"))
532 log_msg("launch_other_browser with uri '%s'\n", uri);
534 if ((urilen = strlen(uri)) > 0) {
535 /* Quote the URI to prevent the shell from interpreting it */
536 /* urilen+3 = length of URI + 2x \' + \0 */
537 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
539 snprintf(quoted_uri, urilen+3, "'%s'", uri);
541 /* If there are any 's in the original URI, URL-escape them
542 (replace them with %27) */
543 quoted_uri_size = urilen + 3;
544 quote = quoted_uri + 1;
545 while ((quote = strchr(quote, '\'')) &&
546 (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
547 /* Check to make sure we don't shrink the memory area
548 as a result of integer overflow */
549 if (quoted_uri_size+2 <= quoted_uri_size)
552 /* Grow the memory area;
553 2 = strlen("%27")-strlen("'") */
554 if (!(quoted_uri = realloc(quoted_uri,
557 quoted_uri_size = quoted_uri_size + 2;
559 /* Recalculate the location of the ' character --
560 realloc() may have moved the string in memory */
561 quote = quoted_uri + offset;
563 /* Move the string after the ', including the \0,
565 memmove(quote+3, quote+1, strlen(quote));
566 memcpy(quote, "%27", 3);
569 urilen = strlen(quoted_uri);
573 cmdlen = strlen(ctx->other_browser_cmd);
575 /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
576 replace "%s"), but is needed in the case other_browser_cmd has no %s
578 if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
580 snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
581 log_msg("command: '%s'\n", command);
583 if (ctx->continuous_mode) {
585 /* Parent process or error in fork() */
595 execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
598 /* Use launch_other_browser as the default browser launcher, with the string
599 passed in as the other_browser_cmd
600 Resulting other_browser_cmd is always safe to free(), even if a pointer
601 to a string constant is passed in */
602 static void use_other_browser_cmd(struct swb_context *ctx, char *cmd) {
603 size_t len = strlen(cmd);
605 free(ctx->other_browser_cmd);
606 ctx->other_browser_cmd = calloc(len+1, sizeof(char));
607 if (!ctx->other_browser_cmd) {
608 log_msg("malloc failed!\n");
609 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
611 ctx->other_browser_cmd = strncpy(ctx->other_browser_cmd,
613 ctx->default_browser_launcher = launch_other_browser;
617 void update_default_browser(struct swb_context *ctx, char *default_browser) {
621 if (!default_browser) {
622 /* No default_browser configured -- use built-in default */
623 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
627 if (!strcmp(default_browser, "tear"))
628 ctx->default_browser_launcher = launch_tear;
629 else if (!strcmp(default_browser, "microb"))
630 ctx->default_browser_launcher = launch_microb;
631 else if (!strcmp(default_browser, "fennec"))
632 /* Cheat and reuse launch_other_browser, since we don't appear
633 to need to do anything special */
634 use_other_browser_cmd(ctx, "fennec %s");
635 else if (!strcmp(default_browser, "midori"))
636 use_other_browser_cmd(ctx, "midori %s");
637 else if (!strcmp(default_browser, "other")) {
638 if (ctx->other_browser_cmd)
639 ctx->default_browser_launcher = launch_other_browser;
641 log_msg("default_browser is 'other', but no other_browser_cmd set -- using default\n");
642 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
645 log_msg("Unknown default_browser %s, using default",
647 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
651 void launch_browser(struct swb_context *ctx, char *uri) {
652 if (ctx && ctx->default_browser_launcher)
653 ctx->default_browser_launcher(ctx, uri);