Fremantle: Coexist with a running MicroB process; don't kill MicroB at end of session...
[browser-switch] / launcher.c
1 /*
2  * launcher.c -- functions for launching web browsers for browser-switchboard
3  *
4  * Copyright (C) 2009-2010 Steven Luo
5  * Derived from a Python implementation by Jason Simpson and Steven Luo
6  *
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.
11  *
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.
16  *
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,
20  * USA.
21  */
22
23 #include <stdlib.h>
24 #include <string.h>
25 #include <stdio.h>
26 #include <errno.h>
27 #include <unistd.h>
28 #include <sys/types.h>
29 #include <sys/wait.h>
30 #include <sys/stat.h>
31 #include <fcntl.h>
32 #include <dbus/dbus-glib.h>
33
34 #ifdef FREMANTLE
35 #include <dbus/dbus.h>
36 #include <signal.h>
37 #include <sys/ptrace.h>
38 #include <sys/inotify.h>
39
40 #define DEFAULT_HOMEDIR "/home/user"
41 #define MICROB_PROFILE_DIR "/.mozilla/microb"
42 #define MICROB_LOCKFILE "lock"
43 #endif
44
45 #include "browser-switchboard.h"
46 #include "launcher.h"
47 #include "dbus-server-bindings.h"
48 #include "log.h"
49
50 struct browser_launcher {
51         char *name;
52         void (*launcher)(struct swb_context *, char *);
53         char *other_browser_cmd;
54         char *binary;
55 };
56
57 #ifdef FREMANTLE
58 static int microb_started = 0;
59 #endif
60
61
62 /* Close stdin/stdout/stderr and replace with /dev/null */
63 static int close_stdio(void) {
64         int fd;
65
66         if ((fd = open("/dev/null", O_RDWR)) == -1)
67                 return -1;
68
69         if (dup2(fd, 0) == -1 || dup2(fd, 1) == -1 || dup2(fd, 2) == -1)
70                 return -1;
71
72         close(fd);
73         return 0;
74 }
75
76
77 static void launch_tear(struct swb_context *ctx, char *uri) {
78         int status;
79         static DBusGProxy *tear_proxy = NULL;
80         GError *error = NULL;
81         pid_t pid;
82
83         if (!uri)
84                 uri = "new_window";
85
86         log_msg("launch_tear with uri '%s'\n", uri);
87
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)) {
96                 if (!tear_proxy) {
97                         if (!(tear_proxy = dbus_g_proxy_new_for_name(
98                                                 ctx->session_bus,
99                                                 "com.nokia.tear",
100                                                 "/com/nokia/tear",
101                                                 "com.nokia.Tear"))) {
102                                 log_msg("Failed to create proxy for com.nokia.Tear D-Bus interface\n");
103                                 exit(1);
104                         }
105                 }
106
107                 if (!dbus_g_proxy_call(tear_proxy, "OpenAddress", &error,
108                                        G_TYPE_STRING, uri, G_TYPE_INVALID,
109                                        G_TYPE_INVALID)) {
110                         log_msg("Opening window failed: %s\n", error->message);
111                         exit(1);
112                 }
113                 if (!ctx->continuous_mode)
114                         exit(0);
115         } else {
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);
120                                 return;
121                         }
122                         /* Child process */
123                         setsid();
124                         close_stdio();
125                 }
126                 execl("/usr/bin/tear", "/usr/bin/tear", uri, (char *)NULL);
127         }
128 }
129
130
131 #ifdef FREMANTLE
132 /* Get a browserd PID from the corresponding Mozilla profile lockfile */
133 static pid_t get_browserd_pid(const char *lockfile) {
134         char buf[256], *tmp;
135
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)
140                 return -errno;
141         if (!(tmp = strstr(buf, ":+")))
142                 return 0;
143         tmp += 2; /* Skip over the ":+" */
144
145         return atoi(tmp);
146 }
147
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
150    works. */
151 static DBusHandlerResult check_microb_started(DBusConnection *connection,
152                                      DBusMessage *message,
153                                      void *user_data) {
154         DBusError error;
155         char *name, *old, *new;
156
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;
167         }
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");
172                 microb_started = 1;
173         }
174
175         return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
176 }
177
178 /* Set up the D-Bus eavesdropping we'll use to watch for MicroB acquiring the
179    com.nokia.osso_browser D-Bus name.
180
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;
187
188         dbus_error_init(&dbus_error);
189
190         conn = dbus_bus_get_private(DBUS_BUS_SESSION, &dbus_error);
191         if (!conn) {
192                 log_msg("Failed to open connection to session bus: %s\n",
193                         dbus_error.message);
194                 dbus_error_free(&dbus_error);
195                 return NULL;
196         }
197
198         dbus_bus_add_match(conn,
199                            "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
200                            &dbus_error);
201         if (dbus_error_is_set(&dbus_error)) {
202                 log_msg("Failed to set up watch for browser UI start: %s\n",
203                         dbus_error.message);
204                 dbus_error_free(&dbus_error);
205                 return NULL;
206         }
207         if (!dbus_connection_add_filter(conn, filter_func, NULL, NULL)) {
208                 log_msg("Failed to set up watch filter!\n");
209                 return NULL;
210         }
211
212         return conn;
213 }
214
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) {
218         microb_started = 0;
219         log_msg("Waiting for MicroB to start\n");
220         while (!microb_started &&
221                dbus_connection_read_write_dispatch(conn, -1));
222 }
223
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;
228
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'",
232                               &dbus_error);
233         if (dbus_error_is_set(&dbus_error))
234                 /* Don't really care -- about to disconnect from the
235                    bus anyhow */
236                 dbus_error_free(&dbus_error);
237         dbus_connection_close(conn);
238         dbus_connection_unref(conn);
239 }
240
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) {
243         pid_t pid;
244         int status;
245
246         status = system("pidof browser > /dev/null");
247         if (WIFEXITED(status) && !WEXITSTATUS(status)) {
248                 /* MicroB browser already running */
249                 return 0;
250         }
251
252         if ((pid = fork()) == -1) {
253                 log_perror(errno, "fork");
254                 return -1;
255         }
256
257         if (!pid) {
258                 /* Child process */
259                 dbus_connection_close(conn);
260                 dbus_connection_unref(conn);
261                 if (fd != -1)
262                         close(fd);
263                 close_stdio();
264
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);
272
273                 /* If we get here, exec() failed */
274                 exit(1);
275         }
276
277         return pid;
278 }
279
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) */
284
285 #define LAUNCH_MICROB_BOOKMARK_WIN_OK 0x1
286
287 int launch_microb_open_window(struct swb_context *ctx, char *uri,
288                               int flags) {
289         DBusGProxy *g_proxy;
290         GError *gerror = NULL;
291
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");
296         if (!g_proxy) {
297                 log_msg("Couldn't get a com.nokia.osso_browser proxy\n");
298                 return 0;
299         }
300
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,
305                                                G_TYPE_INVALID)) {
306                                 log_msg("Opening window failed: %s\n",
307                                         gerror->message);
308                                 g_error_free(gerror);
309                                 return 0;
310                         }
311
312                         return 1;
313                 } else {
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 */
319                         uri = "about:blank";
320                 }
321         }
322         if (!dbus_g_proxy_call(g_proxy, "load_url",
323                                &gerror,
324                                G_TYPE_STRING, uri,
325                                G_TYPE_INVALID,
326                                G_TYPE_INVALID)) {
327                 log_msg("Opening window failed: %s\n", gerror->message);
328                 g_error_free(gerror);
329                 return 0;
330         }
331
332         g_object_unref(g_proxy);
333         return 1;
334 }
335
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) {
338         int status;
339         pid_t pid;
340         char *homedir, *microb_profile_dir, *microb_lockfile;
341         size_t len;
342         int fd, inot_wd;
343         DBusConnection *raw_connection;
344         int bytes_read;
345         char buf[256], *pos;
346         struct inotify_event *event;
347         pid_t browserd_pid, waited_pid;
348         struct sigaction act, oldact;
349         int ignore_sigstop;
350
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");
357                 exit(1);
358         }
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");
365                 exit(1);
366         }
367         snprintf(microb_lockfile, len, "%s%s/%s",
368                  homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
369
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");
376                 exit(1);
377         }
378         if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
379                                          IN_CREATE)) == -1) {
380                 log_perror(errno, "inotify_add_watch");
381                 exit(1);
382         }
383         free(microb_profile_dir);
384
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())) {
390                 exit(1);
391         }
392
393         /* Launch a MicroB browser process if it's not already running */
394         if ((pid = launch_microb_start_browser_process(raw_connection, fd)) < 0)
395                 exit(1);
396
397         /* Release the osso_browser D-Bus name so that MicroB can take it */
398         dbus_release_osso_browser_name(ctx);
399
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
403            window. */
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)) {
407                 exit(1);
408         }
409
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.
416
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
420            appreciated. */
421
422         if (!pid)
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);
426         else
427                 browserd_pid = 0;
428
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) {
437                         pos = buf;
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 */
445                                         pos = NULL;
446                                         break;
447                                 } else if ((pos-buf) + len < bytes_read)
448                                         /* More events to process */
449                                         pos += len;
450                                 else
451                                         /* All events processed */
452                                         break;
453                         }
454                         if (!pos)
455                                 /* Event found, stop looking */
456                                 break;
457                         memset(buf, '\0', 256);
458                 }
459
460                 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
461                         if (browserd_pid == 0)
462                                 log_msg("Profile lockfile link lacks PID\n");
463                         else
464                                 log_perror(-browserd_pid,
465                                            "readlink() on lockfile failed");
466                         exit(1);
467                 }
468         }
469         inotify_rm_watch(fd, inot_wd);
470         close(fd);
471         free(microb_lockfile);
472
473         /* Wait for the browserd to close */
474         log_msg("Waiting for MicroB (browserd pid %d) to finish\n",
475                 browserd_pid);
476         /* Clear any existing SIGCHLD handler to prevent interference
477            with our wait() */
478         act.sa_handler = SIG_DFL;
479         act.sa_flags = 0;
480         sigemptyset(&(act.sa_mask));
481         if (sigaction(SIGCHLD, &act, &oldact) == -1) {
482                 log_perror(errno, "clearing SIGCHLD handler failed");
483                 exit(1);
484         }
485
486         /* Trace the browserd to get a close notification */
487         ignore_sigstop = 1;
488         if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
489                 log_perror(errno, "PTRACE_ATTACH");
490                 exit(1);
491         }
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 */
496                         continue;
497                 if (WIFEXITED(status) || WIFSIGNALED(status))
498                         /* browserd exited */
499                         break;
500                 else if (WIFSTOPPED(status)) {
501                         /* browserd was sent a signal
502                            We're responsible for making sure this signal gets
503                            delivered */
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
509                                  */
510                                 log_msg("Ignoring first SIGSTOP\n");
511                                 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
512                                 ignore_sigstop = 0;
513                                 continue;
514                         }
515                         log_msg("Forwarding signal %d to browserd\n",
516                                 WSTOPSIG(status));
517                         ptrace(PTRACE_CONT, browserd_pid, NULL,
518                                WSTOPSIG(status));
519                 }
520         }
521
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");
528         if (pid > 0) {
529                 kill(pid, SIGTERM);
530                 waitpid(pid, &status, 0);
531         } else {
532                 system("kill `pidof browser` > /dev/null 2>&1");
533         }
534
535         /* Restore old SIGCHLD handler */
536         if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
537                 log_perror(errno, "restoring old SIGCHLD handler failed");
538                 exit(1);
539         }
540
541         dbus_request_osso_browser_name(ctx);
542 }
543
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;
550
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())) {
554                 exit(1);
555         }
556
557         /* Launch a MicroB browser process if it's not already running */
558         if (launch_microb_start_browser_process(raw_connection, -1) < 0)
559                 exit(1);
560
561         /* Release the osso_browser D-Bus name so that MicroB can take it */
562         dbus_release_osso_browser_name(ctx);
563
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)) {
570                 exit(1);
571         }
572
573         /* Take back the osso_browser D-Bus name from MicroB */
574         dbus_request_osso_browser_name(ctx);
575 }
576 #endif /* FREMANTLE */
577
578 void launch_microb(struct swb_context *ctx, char *uri) {
579         int kill_browserd = 0;
580         int status;
581 #ifndef FREMANTLE
582         pid_t pid;
583 #endif
584
585         if (!uri)
586                 uri = "new_window";
587
588         log_msg("launch_microb with uri '%s'\n", uri);
589
590         /* Launch browserd if it's not running */
591         status = system("pidof browserd > /dev/null");
592         if (WIFEXITED(status) && WEXITSTATUS(status)) {
593                 kill_browserd = 1;
594 #ifdef FREMANTLE
595                 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
596 #else
597                 system("/usr/sbin/browserd -d > /dev/null 2>&1");
598 #endif
599         }
600
601 #ifdef FREMANTLE
602         /* Do the insanity to launch Fremantle MicroB */
603         if ((ctx->default_browser_launcher == launch_microb &&
604              ctx->autostart_microb) || ctx->autostart_microb == 1) {
605
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);
610         } else {
611                 /* Otherwise, launch MicroB and kill it when the user's
612                    MicroB session is done */
613                 launch_microb_fremantle_with_kill(ctx, uri);
614         }
615 #else /* !FREMANTLE */
616         /* Release the osso_browser D-Bus name so that MicroB can take it */
617         dbus_release_osso_browser_name(ctx);
618
619         if ((pid = fork()) == -1) {
620                 log_perror(errno, "fork");
621                 exit(1);
622         }
623
624         if (pid > 0) {
625                 /* Parent process */
626                 waitpid(pid, &status, 0);
627         } else {
628                 /* Child process */
629                 close_stdio();
630
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);
637                 } else {
638                         execl("/usr/bin/maemo-invoker",
639                               "browser", "--url", uri, (char *)NULL);
640                 }
641         }
642
643         dbus_request_osso_browser_name(ctx);
644 #endif /* FREMANTLE */
645
646         /* Kill off browserd if we started it */
647         if (kill_browserd)
648                 system("kill `pidof browserd`");
649
650         if (!ctx || !ctx->continuous_mode) 
651                 exit(0);
652 }
653
654 static void launch_other_browser(struct swb_context *ctx, char *uri) {
655         char *command;
656         char *quoted_uri, *quote;
657
658         size_t cmdlen, urilen;
659         size_t quoted_uri_size;
660         size_t offset;
661
662         if (!uri || !strcmp(uri, "new_window"))
663                 uri = "";
664
665         log_msg("launch_other_browser with uri '%s'\n", uri);
666
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))))
671                         exit(1);
672                 snprintf(quoted_uri, urilen+3, "'%s'", uri);
673
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)
683                                 exit(1);
684
685                         /* Grow the memory area;
686                            2 = strlen("%27")-strlen("'") */
687                         if (!(quoted_uri = realloc(quoted_uri,
688                                                    quoted_uri_size+2)))
689                                 exit(1);
690                         quoted_uri_size = quoted_uri_size + 2;
691
692                         /* Recalculate the location of the ' character --
693                            realloc() may have moved the string in memory */
694                         quote = quoted_uri + offset;
695
696                         /* Move the string after the ', including the \0,
697                            over two chars */
698                         memmove(quote+3, quote+1, strlen(quote));
699                         memcpy(quote, "%27", 3);
700                         quote = quote + 3;
701                 }
702                 urilen = strlen(quoted_uri);
703         } else
704                 quoted_uri = uri;
705
706         cmdlen = strlen(ctx->other_browser_cmd);
707
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
710            and urilen < 2 */
711         if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
712                 exit(1);
713         snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
714         log_msg("command: '%s'\n", command);
715
716         if (ctx->continuous_mode) {
717                 if (fork() != 0) {
718                         /* Parent process or error in fork() */
719                         if (urilen > 0)
720                                 free(quoted_uri);
721                         free(command);  
722                         return;
723                 }
724                 /* Child process */
725                 setsid();
726                 close_stdio();
727         }
728         execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
729 }
730
731
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 },
740 };
741
742 static void use_launcher_as_default(struct swb_context *ctx,
743                                     struct browser_launcher *browser) {
744         if (!ctx || !browser)
745                 return;
746
747         if (browser->launcher)
748                 ctx->default_browser_launcher = browser->launcher;
749         else if (browser->other_browser_cmd) {
750                 free(ctx->other_browser_cmd);
751
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 */
759                         exit(1);
760                 } else
761                         ctx->default_browser_launcher = launch_other_browser;
762         }
763
764         return;
765 }
766
767 void update_default_browser(struct swb_context *ctx, char *default_browser) {
768         struct browser_launcher *browser;
769
770         if (!ctx)
771                 return;
772
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]);
776
777         if (!default_browser)
778                 /* No default_browser configured -- use built-in default */
779                 return;
780
781         /* Go through the list of known browser launchers and use one if
782            it matches */
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
786                            system */
787                         if (browser->binary && access(browser->binary, X_OK)) {
788                                 log_msg("%s appears not to be installed\n",
789                                         default_browser);
790                         } else {
791                                 use_launcher_as_default(ctx, browser);
792                                 return;
793                         }
794                 }
795
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;
800                 else
801                         log_msg("default_browser is 'other', but no other_browser_cmd set -- using default\n");
802                 return;
803         }
804
805         /* Unknown value of default_browser */
806         log_msg("Unknown default_browser %s, using default\n", default_browser);
807         return;
808 }
809
810 void launch_browser(struct swb_context *ctx, char *uri) {
811         if (ctx && ctx->default_browser_launcher)
812                 ctx->default_browser_launcher(ctx, uri);
813 }