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