Update webpage
[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_error_init(&dbus_error);
230
231         dbus_connection_remove_filter(conn, filter_func, NULL);
232         dbus_bus_remove_match(conn,
233                               "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
234                               &dbus_error);
235         if (dbus_error_is_set(&dbus_error))
236                 /* Don't really care -- about to disconnect from the
237                    bus anyhow */
238                 dbus_error_free(&dbus_error);
239         dbus_connection_close(conn);
240         dbus_connection_unref(conn);
241 }
242
243 /* Start a new MicroB browser process if one isn't already running */
244 pid_t launch_microb_start_browser_process(DBusConnection *conn, int fd) {
245         pid_t pid;
246         int status;
247
248         status = system("pidof browser > /dev/null");
249         if (WIFEXITED(status) && !WEXITSTATUS(status)) {
250                 /* MicroB browser already running */
251                 return 0;
252         }
253
254         if ((pid = fork()) == -1) {
255                 log_perror(errno, "fork");
256                 return -1;
257         }
258
259         if (!pid) {
260                 /* Child process */
261                 dbus_connection_close(conn);
262                 dbus_connection_unref(conn);
263                 if (fd != -1)
264                         close(fd);
265                 close_stdio();
266
267                 /* exec maemo-invoker directly instead of relying on the
268                    /usr/bin/browser symlink, since /usr/bin/browser may have
269                    been replaced with a shell script calling us via D-Bus */
270                 /* Launch the browser in the background -- our parent will
271                    wait for it to claim the D-Bus name and then display the
272                    window using D-Bus */
273                 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
274
275                 /* If we get here, exec() failed */
276                 exit(1);
277         }
278
279         return pid;
280 }
281
282 /* Open a MicroB window using the D-Bus interface
283    It's assumed that we have already released the D-Bus name and that it's been
284    ensured that MicroB has acquired com.nokia.osso_browser (otherwise this will
285    cause D-Bus to try forever to launch another browser-switchboard) */
286
287 #define LAUNCH_MICROB_BOOKMARK_WIN_OK 0x1
288
289 int launch_microb_open_window(struct swb_context *ctx, char *uri,
290                               int flags) {
291         static DBusGProxy *g_proxy = NULL;
292         GError *gerror = NULL;
293
294         if (!g_proxy) {
295                 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
296                                 "com.nokia.osso_browser",
297                                 "/com/nokia/osso_browser/request",
298                                 "com.nokia.osso_browser");
299                 if (!g_proxy) {
300                         log_msg("Couldn't get a com.nokia.osso_browser proxy\n");
301                         return 0;
302                 }
303         }
304
305         if (!strcmp(uri, "new_window")) {
306                 if (flags & LAUNCH_MICROB_BOOKMARK_WIN_OK) {
307                         if (!dbus_g_proxy_call(g_proxy, "top_application",
308                                                &gerror, G_TYPE_INVALID,
309                                                G_TYPE_INVALID)) {
310                                 log_msg("Opening window failed: %s\n",
311                                         gerror->message);
312                                 g_error_free(gerror);
313                                 return 0;
314                         }
315
316                         return 1;
317                 } else {
318                         /* Since we can't detect when the bookmark window
319                            closes, we'd have a corner case where, if the user
320                            just closes the bookmark window without opening any
321                            browser windows, we don't kill off MicroB or resume
322                            handling com.nokia.osso_browser */
323                         uri = "about:blank";
324                 }
325         }
326         if (!dbus_g_proxy_call(g_proxy, "open_new_window",
327                                &gerror,
328                                G_TYPE_STRING, uri,
329                                G_TYPE_INVALID,
330                                G_TYPE_INVALID)) {
331                 log_msg("Opening window failed: %s\n", gerror->message);
332                 g_error_free(gerror);
333                 return 0;
334         }
335
336         return 1;
337 }
338
339 /* Launch Fremantle MicroB and kill it when the session is finished */
340 void launch_microb_fremantle_with_kill(struct swb_context *ctx, char *uri) {
341         int status;
342         pid_t pid;
343         char *homedir, *microb_profile_dir, *microb_lockfile;
344         size_t len;
345         int fd, inot_wd;
346         DBusConnection *raw_connection;
347         int bytes_read;
348         char buf[256], *pos;
349         struct inotify_event *event;
350         pid_t browserd_pid, waited_pid;
351         struct sigaction act, oldact;
352         int ignore_sigstop;
353
354         /* Put together the path to the MicroB browserd lockfile */
355         if (!(homedir = getenv("HOME")))
356                 homedir = DEFAULT_HOMEDIR;
357         len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
358         if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
359                 log_msg("calloc() failed\n");
360                 exit(1);
361         }
362         snprintf(microb_profile_dir, len, "%s%s",
363                  homedir, MICROB_PROFILE_DIR);
364         len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
365               strlen("/") + strlen(MICROB_LOCKFILE) + 1;
366         if (!(microb_lockfile = calloc(len, sizeof(char)))) {
367                 log_msg("calloc() failed\n");
368                 exit(1);
369         }
370         snprintf(microb_lockfile, len, "%s%s/%s",
371                  homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
372
373         /* Watch for the creation of a MicroB browserd lockfile
374            NB: The watch has to be set up here, before the browser
375            is launched, to make sure there's no race between browserd
376            starting and us creating the watch */
377         if ((fd = inotify_init()) == -1) {
378                 log_perror(errno, "inotify_init");
379                 exit(1);
380         }
381         if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
382                                          IN_CREATE)) == -1) {
383                 log_perror(errno, "inotify_add_watch");
384                 exit(1);
385         }
386         free(microb_profile_dir);
387
388         /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
389            acquiring the com.nokia.osso_browser D-Bus name.  Again, this needs
390            to happen before the browser is launched, so that there's no race
391            between establishing the watch and browser startup. */
392         if (!(raw_connection = microb_start_dbus_watch_init())) {
393                 exit(1);
394         }
395
396         /* Launch a MicroB browser process if it's not already running */
397         if ((pid = launch_microb_start_browser_process(raw_connection, fd)) < 0)
398                 exit(1);
399
400         /* Release the osso_browser D-Bus name so that MicroB can take it */
401         dbus_release_osso_browser_name(ctx);
402
403         /* Wait for our child to start the browser UI process and
404            for it to acquire the com.nokia.osso_browser D-Bus name,
405            then make the appropriate method call to open the browser
406            window. */
407         microb_start_dbus_watch_wait(raw_connection);
408         microb_start_dbus_watch_remove(raw_connection);
409         if (!launch_microb_open_window(ctx, uri, 0)) {
410                 exit(1);
411         }
412
413         /* Workaround: the browser process we started is going to want
414            to hang around forever, hogging the com.nokia.osso_browser
415            D-Bus interface while at it.  To fix this, we notice that
416            when the last browser window closes, the browser UI restarts
417            its attached browserd process.  Get the browserd process's
418            PID and use ptrace() to watch for process termination.
419
420            This has the problem of not being able to detect whether
421            the bookmark window is open and/or in use, but it's the best
422            that I can think of.  Better suggestions would be greatly
423            appreciated. */
424
425         if (!pid)
426                 /* If we didn't start the MicroB browser process ourselves, try
427                    to get the PID of the browserd from the lockfile */
428                 browserd_pid = get_browserd_pid(microb_lockfile);
429         else
430                 browserd_pid = 0;
431
432         /* If getting the lockfile PID failed, or the lockfile PID doesn't
433            exist, assume that we have a stale lockfile and wait for the new
434            browserd lockfile to be created */
435         if (browserd_pid <= 0 || kill(browserd_pid, 0) == ESRCH) {
436                 log_msg("Waiting for browserd lockfile to be created\n");
437                 memset(buf, '\0', 256);
438                 /* read() blocks until there are events to be read */
439                 while ((bytes_read = read(fd, buf, 255)) > 0) {
440                         pos = buf;
441                         /* Loop until we see the event we're looking for
442                            or until all the events are processed */
443                         while (pos && (pos-buf) < bytes_read) {
444                                 event = (struct inotify_event *)pos;
445                                 len = sizeof(struct inotify_event) + event->len;
446                                 if (!strcmp(MICROB_LOCKFILE, event->name)) {
447                                         /* Lockfile created */
448                                         pos = NULL;
449                                         break;
450                                 } else if ((pos-buf) + len < bytes_read)
451                                         /* More events to process */
452                                         pos += len;
453                                 else
454                                         /* All events processed */
455                                         break;
456                         }
457                         if (!pos)
458                                 /* Event found, stop looking */
459                                 break;
460                         memset(buf, '\0', 256);
461                 }
462
463                 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
464                         if (browserd_pid == 0)
465                                 log_msg("Profile lockfile link lacks PID\n");
466                         else
467                                 log_perror(-browserd_pid,
468                                            "readlink() on lockfile failed");
469                         exit(1);
470                 }
471         }
472         inotify_rm_watch(fd, inot_wd);
473         close(fd);
474         free(microb_lockfile);
475
476         /* Wait for the browserd to close */
477         log_msg("Waiting for MicroB (browserd pid %d) to finish\n",
478                 browserd_pid);
479         /* Clear any existing SIGCHLD handler to prevent interference
480            with our wait() */
481         act.sa_handler = SIG_DFL;
482         act.sa_flags = 0;
483         sigemptyset(&(act.sa_mask));
484         if (sigaction(SIGCHLD, &act, &oldact) == -1) {
485                 log_perror(errno, "clearing SIGCHLD handler failed");
486                 exit(1);
487         }
488
489         /* Trace the browserd to get a close notification */
490         ignore_sigstop = 1;
491         if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
492                 log_perror(errno, "PTRACE_ATTACH");
493                 exit(1);
494         }
495         ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
496         while ((waited_pid = wait(&status)) > 0) {
497                 if (waited_pid != browserd_pid)
498                         /* Not interested in other processes */
499                         continue;
500                 if (WIFEXITED(status) || WIFSIGNALED(status))
501                         /* browserd exited */
502                         break;
503                 else if (WIFSTOPPED(status)) {
504                         /* browserd was sent a signal
505                            We're responsible for making sure this signal gets
506                            delivered */
507                         if (ignore_sigstop && WSTOPSIG(status) == SIGSTOP) {
508                                 /* Ignore the first SIGSTOP received
509                                    This is raised for some reason immediately
510                                    after we start tracing the process, and
511                                    won't be followed by a SIGCONT at any point
512                                  */
513                                 log_msg("Ignoring first SIGSTOP\n");
514                                 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
515                                 ignore_sigstop = 0;
516                                 continue;
517                         }
518                         log_msg("Forwarding signal %d to browserd\n",
519                                 WSTOPSIG(status));
520                         ptrace(PTRACE_CONT, browserd_pid, NULL,
521                                WSTOPSIG(status));
522                 }
523         }
524
525         /* Kill off browser UI
526            XXX: There is a race here with the restarting of the closed
527            browserd; if that happens before we kill the browser UI, the newly
528            started browserd may not close with the UI
529            XXX: Hope we don't cause data loss here! */
530         log_msg("Killing MicroB\n");
531         if (pid > 0) {
532                 kill(pid, SIGTERM);
533                 waitpid(pid, &status, 0);
534         } else {
535                 system("kill `pidof browser` > /dev/null 2>&1");
536         }
537
538         /* Restore old SIGCHLD handler */
539         if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
540                 log_perror(errno, "restoring old SIGCHLD handler failed");
541                 exit(1);
542         }
543
544         dbus_request_osso_browser_name(ctx);
545 }
546
547 /* Launch a new window in Fremantle MicroB; don't kill the MicroB process
548    when the session is finished
549    This is designed to work with a prestarted MicroB process that runs
550    continuously in the background */
551 void launch_microb_fremantle(struct swb_context *ctx, char *uri) {
552         DBusConnection *raw_connection;
553
554         /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
555            acquiring the com.nokia.osso_browser D-Bus name */
556         if (!(raw_connection = microb_start_dbus_watch_init())) {
557                 exit(1);
558         }
559
560         /* Launch a MicroB browser process if it's not already running */
561         if (launch_microb_start_browser_process(raw_connection, -1) < 0)
562                 exit(1);
563
564         /* Release the osso_browser D-Bus name so that MicroB can take it */
565         dbus_release_osso_browser_name(ctx);
566
567         /* Wait for MicroB to acquire com.nokia.osso_browser, then make the
568            appropriate method call to open the browser window. */
569         microb_start_dbus_watch_wait(raw_connection);
570         microb_start_dbus_watch_remove(raw_connection);
571         if (!launch_microb_open_window(ctx, uri,
572                                        LAUNCH_MICROB_BOOKMARK_WIN_OK)) {
573                 exit(1);
574         }
575
576         /* Take back the osso_browser D-Bus name from MicroB */
577         dbus_request_osso_browser_name(ctx);
578 }
579 #endif /* FREMANTLE */
580
581 void launch_microb(struct swb_context *ctx, char *uri) {
582         int kill_browserd = 0;
583         int status;
584 #ifndef FREMANTLE
585         pid_t pid;
586 #endif
587
588         if (!uri)
589                 uri = "new_window";
590
591         log_msg("launch_microb with uri '%s'\n", uri);
592
593         /* Launch browserd if it's not running */
594         status = system("pidof browserd > /dev/null");
595         if (WIFEXITED(status) && WEXITSTATUS(status)) {
596                 kill_browserd = 1;
597 #ifdef FREMANTLE
598                 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
599 #else
600                 system("/usr/sbin/browserd -d > /dev/null 2>&1");
601 #endif
602         }
603
604 #ifdef FREMANTLE
605         /* Do the insanity to launch Fremantle MicroB */
606         if ((ctx->default_browser_launcher == launch_microb &&
607              ctx->autostart_microb) || ctx->autostart_microb == 1) {
608
609                 /* If MicroB is set as the default browser, or if the user has
610                    configured MicroB to always be running, just send the
611                    running MicroB the request */
612                 launch_microb_fremantle(ctx, uri);
613         } else {
614                 /* Otherwise, launch MicroB and kill it when the user's
615                    MicroB session is done */
616                 launch_microb_fremantle_with_kill(ctx, uri);
617         }
618 #else /* !FREMANTLE */
619         /* Release the osso_browser D-Bus name so that MicroB can take it */
620         dbus_release_osso_browser_name(ctx);
621
622         if ((pid = fork()) == -1) {
623                 log_perror(errno, "fork");
624                 exit(1);
625         }
626
627         if (pid > 0) {
628                 /* Parent process */
629                 waitpid(pid, &status, 0);
630         } else {
631                 /* Child process */
632                 close_stdio();
633
634                 /* exec maemo-invoker directly instead of relying on the
635                    /usr/bin/browser symlink, since /usr/bin/browser may have
636                    been replaced with a shell script calling us via D-Bus */
637                 if (!strcmp(uri, "new_window")) {
638                         execl("/usr/bin/maemo-invoker",
639                               "browser", (char *)NULL);
640                 } else {
641                         execl("/usr/bin/maemo-invoker",
642                               "browser", "--url", uri, (char *)NULL);
643                 }
644         }
645
646         dbus_request_osso_browser_name(ctx);
647 #endif /* FREMANTLE */
648
649         /* Kill off browserd if we started it */
650         if (kill_browserd)
651                 system("kill `pidof browserd`");
652
653         if (!ctx || !ctx->continuous_mode)
654                 exit(0);
655 }
656
657 static void launch_other_browser(struct swb_context *ctx, char *uri) {
658         char *command;
659         char *quoted_uri, *quote;
660
661         size_t cmdlen, urilen;
662         size_t quoted_uri_size;
663         size_t offset;
664
665         if (!uri || !strcmp(uri, "new_window"))
666                 uri = "";
667
668         log_msg("launch_other_browser with uri '%s'\n", uri);
669
670         if ((urilen = strlen(uri)) > 0) {
671                 /* Quote the URI to prevent the shell from interpreting it */
672                 /* urilen+3 = length of URI + 2x \' + \0 */
673                 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
674                         exit(1);
675                 snprintf(quoted_uri, urilen+3, "'%s'", uri);
676
677                 /* If there are any 's in the original URI, URL-escape them
678                    (replace them with %27) */
679                 quoted_uri_size = urilen + 3;
680                 quote = quoted_uri + 1;
681                 while ((quote = strchr(quote, '\'')) &&
682                        (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
683                         /* Check to make sure we don't shrink the memory area
684                            as a result of integer overflow */
685                         if (quoted_uri_size+2 <= quoted_uri_size)
686                                 exit(1);
687
688                         /* Grow the memory area;
689                            2 = strlen("%27")-strlen("'") */
690                         if (!(quoted_uri = realloc(quoted_uri,
691                                                    quoted_uri_size+2)))
692                                 exit(1);
693                         quoted_uri_size = quoted_uri_size + 2;
694
695                         /* Recalculate the location of the ' character --
696                            realloc() may have moved the string in memory */
697                         quote = quoted_uri + offset;
698
699                         /* Move the string after the ', including the \0,
700                            over two chars */
701                         memmove(quote+3, quote+1, strlen(quote));
702                         memcpy(quote, "%27", 3);
703                         quote = quote + 3;
704                 }
705                 urilen = strlen(quoted_uri);
706         } else
707                 quoted_uri = uri;
708
709         cmdlen = strlen(ctx->other_browser_cmd);
710
711         /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
712            replace "%s"), but is needed in the case other_browser_cmd has no %s
713            and urilen < 2 */
714         if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
715                 exit(1);
716         snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
717         log_msg("command: '%s'\n", command);
718
719         if (ctx->continuous_mode) {
720                 if (fork() != 0) {
721                         /* Parent process or error in fork() */
722                         if (urilen > 0)
723                                 free(quoted_uri);
724                         free(command);
725                         return;
726                 }
727                 /* Child process */
728                 setsid();
729                 close_stdio();
730         }
731         execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
732 }
733
734
735 /* The list of known browsers and how to launch them */
736 static struct browser_launcher browser_launchers[] = {
737         { "microb", launch_microb, NULL, NULL }, /* First entry is the default! */
738         { "tear", launch_tear, NULL, "/usr/bin/tear" },
739         { "fennec", NULL, "fennec %s", "/usr/bin/fennec" },
740         { "opera", NULL, "opera %s", "/usr/bin/opera" },
741         { "midori", NULL, "midori %s", "/usr/bin/midori" },
742         { NULL, NULL, NULL, NULL },
743 };
744
745 static void use_launcher_as_default(struct swb_context *ctx,
746                                     struct browser_launcher *browser) {
747         if (!ctx || !browser)
748                 return;
749
750         if (browser->launcher)
751                 ctx->default_browser_launcher = browser->launcher;
752         else if (browser->other_browser_cmd) {
753                 free(ctx->other_browser_cmd);
754
755                 /* Make a copy of the string constant so that
756                    ctx->other_browser_cmd is safe to free() */
757                 ctx->other_browser_cmd = strdup(browser->other_browser_cmd);
758                 if (!ctx->other_browser_cmd) {
759                         log_msg("malloc failed!\n");
760                         /* Ideally, we'd configure the built-in default here --
761                            but it's possible we could be called in that path */
762                         exit(1);
763                 } else
764                         ctx->default_browser_launcher = launch_other_browser;
765         }
766
767         return;
768 }
769
770 void update_default_browser(struct swb_context *ctx, char *default_browser) {
771         struct browser_launcher *browser;
772
773         if (!ctx)
774                 return;
775
776         /* Configure the built-in default to start -- that way, we can
777            handle errors by just returning */
778         use_launcher_as_default(ctx, &browser_launchers[0]);
779
780         if (!default_browser)
781                 /* No default_browser configured -- use built-in default */
782                 return;
783
784         /* Go through the list of known browser launchers and use one if
785            it matches */
786         for (browser = browser_launchers; browser->name; ++browser)
787                 if (!strcmp(default_browser, browser->name)) {
788                         /* Make sure the user's choice is installed on the
789                            system */
790                         if (browser->binary && access(browser->binary, X_OK)) {
791                                 log_msg("%s appears not to be installed\n",
792                                         default_browser);
793                         } else {
794                                 use_launcher_as_default(ctx, browser);
795                                 return;
796                         }
797                 }
798
799         /* Deal with default_browser = "other" */
800         if (!strcmp(default_browser, "other")) {
801                 if (ctx->other_browser_cmd)
802                         ctx->default_browser_launcher = launch_other_browser;
803                 else
804                         log_msg("default_browser is 'other', but no other_browser_cmd set -- using default\n");
805                 return;
806         }
807
808         /* Unknown value of default_browser */
809         log_msg("Unknown default_browser %s, using default\n", default_browser);
810         return;
811 }
812
813 void launch_browser(struct swb_context *ctx, char *uri) {
814         if (ctx && ctx->default_browser_launcher)
815                 ctx->default_browser_launcher(ctx, uri);
816 }