Update debian/rules to build command-line config utility
[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
60 /* Check to see whether MicroB is ready to handle D-Bus requests yet
61    See the comments in launch_microb to understand how this works. */
62 static DBusHandlerResult check_microb_started(DBusConnection *connection,
63                                      DBusMessage *message,
64                                      void *user_data) {
65         DBusError error;
66         char *name, *old, *new;
67
68         log_msg("Checking to see if MicroB is ready\n");
69         dbus_error_init(&error);
70         if (!dbus_message_get_args(message, &error,
71                                    DBUS_TYPE_STRING, &name,
72                                    DBUS_TYPE_STRING, &old,
73                                    DBUS_TYPE_STRING, &new,
74                                    DBUS_TYPE_INVALID)) {
75                 log_msg("%s\n", error.message);
76                 dbus_error_free(&error);
77                 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
78         }
79         /* If old is an empty string, then the name has been acquired, and
80            MicroB should be ready to handle our request */
81         if (strlen(old) == 0) {
82                 log_msg("MicroB ready\n");
83                 microb_started = 1;
84         }
85
86         return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
87 }
88
89 /* Get a browserd PID from the corresponding Mozilla profile lockfile */
90 static pid_t get_browserd_pid(const char *lockfile) {
91         char buf[256], *tmp;
92
93         /* The lockfile is a symlink pointing to "[ipaddr]:+[pid]", so read in
94            the target of the symlink and parse it that way */
95         memset(buf, '\0', 256);
96         if (readlink(lockfile, buf, 255) == -1)
97                 return -errno;
98         if (!(tmp = strstr(buf, ":+")))
99                 return 0;
100         tmp += 2; /* Skip over the ":+" */
101
102         return atoi(tmp);
103 }
104 #endif
105
106 /* Close stdin/stdout/stderr and replace with /dev/null */
107 static int close_stdio(void) {
108         int fd;
109
110         if ((fd = open("/dev/null", O_RDWR)) == -1)
111                 return -1;
112
113         if (dup2(fd, 0) == -1 || dup2(fd, 1) == -1 || dup2(fd, 2) == -1)
114                 return -1;
115
116         close(fd);
117         return 0;
118 }
119
120 static void launch_tear(struct swb_context *ctx, char *uri) {
121         int status;
122         static DBusGProxy *tear_proxy = NULL;
123         GError *error = NULL;
124         pid_t pid;
125
126         if (!uri)
127                 uri = "new_window";
128
129         log_msg("launch_tear with uri '%s'\n", uri);
130
131         /* We should be able to just call the D-Bus service to open Tear ...
132            but if Tear's not open, that cuases D-Bus to start Tear and then
133            pass it the OpenAddress call, which results in two browser windows.
134            Properly fixing this probably requires Tear to provide a D-Bus
135            method that opens an address in an existing window, but for now work
136            around by just invoking Tear with exec() if it's not running. */
137         status = system("pidof tear > /dev/null");
138         if (WIFEXITED(status) && !WEXITSTATUS(status)) {
139                 if (!tear_proxy) {
140                         if (!(tear_proxy = dbus_g_proxy_new_for_name(
141                                                 ctx->session_bus,
142                                                 "com.nokia.tear",
143                                                 "/com/nokia/tear",
144                                                 "com.nokia.Tear"))) {
145                                 log_msg("Failed to create proxy for com.nokia.Tear D-Bus interface\n");
146                                 exit(1);
147                         }
148                 }
149
150                 if (!dbus_g_proxy_call(tear_proxy, "OpenAddress", &error,
151                                        G_TYPE_STRING, uri, G_TYPE_INVALID,
152                                        G_TYPE_INVALID)) {
153                         log_msg("Opening window failed: %s\n", error->message);
154                         exit(1);
155                 }
156                 if (!ctx->continuous_mode)
157                         exit(0);
158         } else {
159                 if (ctx->continuous_mode) {
160                         if ((pid = fork()) != 0) {
161                                 /* Parent process or error in fork() */
162                                 log_msg("child: %d\n", (int)pid);
163                                 return;
164                         }
165                         /* Child process */
166                         setsid();
167                         close_stdio();
168                 }
169                 execl("/usr/bin/tear", "/usr/bin/tear", uri, (char *)NULL);
170         }
171 }
172
173 void launch_microb(struct swb_context *ctx, char *uri) {
174         int kill_browserd = 0;
175         int status;
176         pid_t pid;
177 #ifdef FREMANTLE
178         char *homedir, *microb_profile_dir, *microb_lockfile;
179         size_t len;
180         int fd, inot_wd;
181         DBusConnection *raw_connection;
182         DBusError dbus_error;
183         DBusHandleMessageFunction filter_func;
184         DBusGProxy *g_proxy;
185         GError *gerror = NULL;
186         int bytes_read;
187         char buf[256], *pos;
188         struct inotify_event *event;
189         pid_t browserd_pid, waited_pid;
190         struct sigaction act, oldact;
191         int ignore_sigstop;
192 #endif
193
194         if (!uri)
195                 uri = "new_window";
196
197         log_msg("launch_microb with uri '%s'\n", uri);
198
199         /* Launch browserd if it's not running */
200         status = system("pidof browserd > /dev/null");
201         if (WIFEXITED(status) && WEXITSTATUS(status)) {
202                 kill_browserd = 1;
203 #ifdef FREMANTLE
204                 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
205 #else
206                 system("/usr/sbin/browserd -d > /dev/null 2>&1");
207 #endif
208         }
209
210         /* Release the osso_browser D-Bus name so that MicroB can take it */
211         dbus_release_osso_browser_name(ctx);
212
213 #ifdef FREMANTLE
214         /* Put together the path to the MicroB browserd lockfile */
215         if (!(homedir = getenv("HOME")))
216                 homedir = DEFAULT_HOMEDIR;
217         len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
218         if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
219                 log_msg("calloc() failed\n");
220                 exit(1);
221         }
222         snprintf(microb_profile_dir, len, "%s%s",
223                  homedir, MICROB_PROFILE_DIR);
224         len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
225               strlen("/") + strlen(MICROB_LOCKFILE) + 1;
226         if (!(microb_lockfile = calloc(len, sizeof(char)))) {
227                 log_msg("calloc() failed\n");
228                 exit(1);
229         }
230         snprintf(microb_lockfile, len, "%s%s/%s",
231                  homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
232
233         /* Watch for the creation of a MicroB browserd lockfile
234            NB: The watch has to be set up here, before the browser
235            is launched, to make sure there's no race between browserd
236            starting and us creating the watch */
237         if ((fd = inotify_init()) == -1) {
238                 log_perror(errno, "inotify_init");
239                 exit(1);
240         }
241         if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
242                                          IN_CREATE)) == -1) {
243                 log_perror(errno, "inotify_add_watch");
244                 exit(1);
245         }
246         free(microb_profile_dir);
247
248         /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
249            acquiring the com.nokia.osso_browser D-Bus name.  Again, this needs
250            to happen before the browser is launched, so that there's no race
251            between establishing the watch and browser startup.
252
253            Ideas for how to do this monitoring derived from the dbus-monitor
254            code (tools/dbus-monitor.c in the D-Bus codebase). */
255         dbus_error_init(&dbus_error);
256
257         raw_connection = dbus_bus_get_private(DBUS_BUS_SESSION, &dbus_error);
258         if (!raw_connection) {
259                 log_msg("Failed to open connection to session bus: %s\n",
260                         dbus_error.message);
261                 dbus_error_free(&dbus_error);
262                 exit(1);
263         }
264
265         dbus_bus_add_match(raw_connection,
266                            "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
267                            &dbus_error);
268         if (dbus_error_is_set(&dbus_error)) {
269                 log_msg("Failed to set up watch for browser UI start: %s\n",
270                         dbus_error.message);
271                 dbus_error_free(&dbus_error);
272                 exit(1);
273         }
274         filter_func = check_microb_started;
275         if (!dbus_connection_add_filter(raw_connection,
276                                         filter_func, NULL, NULL)) {
277                 log_msg("Failed to set up watch filter!\n");
278                 exit(1);
279         }
280
281         if ((pid = fork()) == -1) {
282                 log_perror(errno, "fork");
283                 exit(1);
284         }
285
286         if (pid > 0) {
287                 /* Parent process */
288                 /* Wait for our child to start the browser UI process and
289                    for it to acquire the com.nokia.osso_browser D-Bus name,
290                    then make the appropriate method call to open the browser
291                    window. */
292                 microb_started = 0;
293                 log_msg("Waiting for MicroB to start\n");
294                 while (!microb_started &&
295                        dbus_connection_read_write_dispatch(raw_connection,
296                                                            -1));
297                 dbus_connection_remove_filter(raw_connection,
298                                               filter_func, NULL);
299                 dbus_bus_remove_match(raw_connection,
300                                       "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
301                                       &dbus_error);
302                 if (dbus_error_is_set(&dbus_error))
303                         /* Don't really care -- about to disconnect from the
304                            bus anyhow */
305                         dbus_error_free(&dbus_error);
306                 dbus_connection_close(raw_connection);
307                 dbus_connection_unref(raw_connection);
308
309                 /* Browser UI's started, send it the request for a new window
310                    via D-Bus */
311                 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
312                                 "com.nokia.osso_browser",
313                                 "/com/nokia/osso_browser/request",
314                                 "com.nokia.osso_browser");
315                 if (!g_proxy) {
316                         log_msg("Couldn't get a com.nokia.osso_browser proxy\n");
317                         exit(1);
318                 }
319                 if (!strcmp(uri, "new_window")) {
320 #if 0 /* Since we can't detect when the bookmark window closes, we'd have a
321          corner case where, if the user just closes the bookmark window
322          without opening any browser windows, we don't kill off MicroB or
323          resume handling com.nokia.osso_browser */
324                         if (!dbus_g_proxy_call(g_proxy, "top_application",
325                                                &gerror, G_TYPE_INVALID,
326                                                G_TYPE_INVALID)) {
327                                 log_msg("Opening window failed: %s\n",
328                                         gerror->message);
329                                 exit(1);
330                         }
331 #endif
332                         if (!dbus_g_proxy_call(g_proxy, "load_url",
333                                                &gerror,
334                                                G_TYPE_STRING, "about:blank",
335                                                G_TYPE_INVALID,
336                                                G_TYPE_INVALID)) {
337                                 log_msg("Opening window failed: %s\n",
338                                         gerror->message);
339                                 exit(1);
340                         }
341                 } else {
342                         if (!dbus_g_proxy_call(g_proxy, "load_url",
343                                                &gerror,
344                                                G_TYPE_STRING, uri,
345                                                G_TYPE_INVALID,
346                                                G_TYPE_INVALID)) {
347                                 log_msg("Opening window failed: %s\n",
348                                         gerror->message);
349                                 exit(1);
350                         }
351                 }
352                 g_object_unref(g_proxy);
353
354                 /* Workaround: the browser process we started is going to want
355                    to hang around forever, hogging the com.nokia.osso_browser
356                    D-Bus interface while at it.  To fix this, we notice that
357                    when the last browser window closes, the browser UI restarts
358                    its attached browserd process.  Get the browserd process's
359                    PID and use ptrace() to watch for process termination.
360
361                    This has the problem of not being able to detect whether
362                    the bookmark window is open and/or in use, but it's the best
363                    that I can think of.  Better suggestions would be greatly
364                    appreciated. */
365
366                 /* Wait for the MicroB browserd lockfile to be created */
367                 log_msg("Waiting for browserd lockfile to be created\n");
368                 memset(buf, '\0', 256);
369                 /* read() blocks until there are events to be read */
370                 while ((bytes_read = read(fd, buf, 255)) > 0) {
371                         pos = buf;
372                         /* Loop until we see the event we're looking for
373                            or until all the events are processed */
374                         while (pos && (pos-buf) < bytes_read) {
375                                 event = (struct inotify_event *)pos;
376                                 len = sizeof(struct inotify_event)
377                                       + event->len;
378                                 if (!strcmp(MICROB_LOCKFILE,
379                                             event->name)) {
380                                         /* Lockfile created */
381                                         pos = NULL;
382                                         break;
383                                 } else if ((pos-buf) + len < bytes_read)
384                                         /* More events to process */
385                                         pos += len;
386                                 else
387                                         /* All events processed */
388                                         break;
389                         }
390                         if (!pos)
391                                 /* Event found, stop looking */
392                                 break;
393                         memset(buf, '\0', 256);
394                 }
395                 inotify_rm_watch(fd, inot_wd);
396                 close(fd);
397
398                 /* Get the PID of the browserd from the lockfile */
399                 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
400                         if (browserd_pid == 0)
401                                 log_msg("Profile lockfile link lacks PID\n");
402                         else
403                                 log_perror(-browserd_pid,
404                                            "readlink() on lockfile failed");
405                         exit(1);
406                 }
407                 free(microb_lockfile);
408
409                 /* Wait for the browserd to close */
410                 log_msg("Waiting for MicroB (browserd pid %d) to finish\n",
411                         browserd_pid);
412                 /* Clear any existing SIGCHLD handler to prevent interference
413                    with our wait() */
414                 act.sa_handler = SIG_DFL;
415                 act.sa_flags = 0;
416                 sigemptyset(&(act.sa_mask));
417                 if (sigaction(SIGCHLD, &act, &oldact) == -1) {
418                         log_perror(errno, "clearing SIGCHLD handler failed");
419                         exit(1);
420                 }
421
422                 /* Trace the browserd to get a close notification */
423                 ignore_sigstop = 1;
424                 if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
425                         log_perror(errno, "PTRACE_ATTACH");
426                         exit(1);
427                 }
428                 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
429                 while ((waited_pid = wait(&status)) > 0) {
430                         if (waited_pid != browserd_pid)
431                                 /* Not interested in other processes */
432                                 continue;
433                         if (WIFEXITED(status) || WIFSIGNALED(status))
434                                 /* browserd exited */
435                                 break;
436                         else if (WIFSTOPPED(status)) {
437                                 /* browserd was sent a signal
438                                    We're responsible for making sure this
439                                    signal gets delivered */
440                                 if (ignore_sigstop &&
441                                     WSTOPSIG(status) == SIGSTOP) {
442                                         /* Ignore the first SIGSTOP received
443                                            This is raised for some reason
444                                            immediately after we start tracing
445                                            the process, and won't be followed
446                                            by a SIGCONT at any point */
447                                         log_msg("Ignoring first SIGSTOP\n");
448                                         ptrace(PTRACE_CONT, browserd_pid,
449                                                NULL, NULL);
450                                         ignore_sigstop = 0;
451                                         continue;
452                                 }
453                                 log_msg("Forwarding signal %d to browserd\n",
454                                         WSTOPSIG(status));
455                                 ptrace(PTRACE_CONT, browserd_pid,
456                                        NULL, WSTOPSIG(status));
457                         }
458                 }
459
460                 /* Kill off browser UI
461                    XXX: There is a race here with the restarting of the closed
462                    browserd; if that happens before we kill the browser UI, the
463                    newly started browserd may not close with the UI
464                    XXX: Hope we don't cause data loss here! */
465                 log_msg("Killing MicroB\n");
466                 kill(pid, SIGTERM);
467                 waitpid(pid, &status, 0);
468
469                 /* Restore old SIGCHLD handler */
470                 if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
471                         log_perror(errno,
472                                    "restoring old SIGCHLD handler failed");
473                         exit(1);
474                 }
475         } else {
476                 /* Child process */
477                 dbus_connection_close(raw_connection);
478                 dbus_connection_unref(raw_connection);
479                 close(fd);
480                 close_stdio();
481
482                 /* exec maemo-invoker directly instead of relying on the
483                    /usr/bin/browser symlink, since /usr/bin/browser may have
484                    been replaced with a shell script calling us via D-Bus */
485                 /* Launch the browser in the background -- our parent will
486                    wait for it to claim the D-Bus name and then display the
487                    window using D-Bus */
488                 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
489         }
490 #else /* !FREMANTLE */
491         if ((pid = fork()) == -1) {
492                 log_perror(errno, "fork");
493                 exit(1);
494         }
495
496         if (pid > 0) {
497                 /* Parent process */
498                 waitpid(pid, &status, 0);
499         } else {
500                 /* Child process */
501                 close_stdio();
502
503                 /* exec maemo-invoker directly instead of relying on the
504                    /usr/bin/browser symlink, since /usr/bin/browser may have
505                    been replaced with a shell script calling us via D-Bus */
506                 if (!strcmp(uri, "new_window")) {
507                         execl("/usr/bin/maemo-invoker",
508                               "browser", (char *)NULL);
509                 } else {
510                         execl("/usr/bin/maemo-invoker",
511                               "browser", "--url", uri, (char *)NULL);
512                 }
513         }
514 #endif /* FREMANTLE */
515
516         /* Kill off browserd if we started it */
517         if (kill_browserd)
518                 system("kill `pidof browserd`");
519
520         if (!ctx || !ctx->continuous_mode) 
521                 exit(0);
522
523         dbus_request_osso_browser_name(ctx);
524 }
525
526 static void launch_other_browser(struct swb_context *ctx, char *uri) {
527         char *command;
528         char *quoted_uri, *quote;
529
530         size_t cmdlen, urilen;
531         size_t quoted_uri_size;
532         size_t offset;
533
534         if (!uri || !strcmp(uri, "new_window"))
535                 uri = "";
536
537         log_msg("launch_other_browser with uri '%s'\n", uri);
538
539         if ((urilen = strlen(uri)) > 0) {
540                 /* Quote the URI to prevent the shell from interpreting it */
541                 /* urilen+3 = length of URI + 2x \' + \0 */
542                 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
543                         exit(1);
544                 snprintf(quoted_uri, urilen+3, "'%s'", uri);
545
546                 /* If there are any 's in the original URI, URL-escape them
547                    (replace them with %27) */
548                 quoted_uri_size = urilen + 3;
549                 quote = quoted_uri + 1;
550                 while ((quote = strchr(quote, '\'')) &&
551                        (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
552                         /* Check to make sure we don't shrink the memory area
553                            as a result of integer overflow */
554                         if (quoted_uri_size+2 <= quoted_uri_size)
555                                 exit(1);
556
557                         /* Grow the memory area;
558                            2 = strlen("%27")-strlen("'") */
559                         if (!(quoted_uri = realloc(quoted_uri,
560                                                    quoted_uri_size+2)))
561                                 exit(1);
562                         quoted_uri_size = quoted_uri_size + 2;
563
564                         /* Recalculate the location of the ' character --
565                            realloc() may have moved the string in memory */
566                         quote = quoted_uri + offset;
567
568                         /* Move the string after the ', including the \0,
569                            over two chars */
570                         memmove(quote+3, quote+1, strlen(quote));
571                         memcpy(quote, "%27", 3);
572                         quote = quote + 3;
573                 }
574                 urilen = strlen(quoted_uri);
575         } else
576                 quoted_uri = uri;
577
578         cmdlen = strlen(ctx->other_browser_cmd);
579
580         /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
581            replace "%s"), but is needed in the case other_browser_cmd has no %s
582            and urilen < 2 */
583         if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
584                 exit(1);
585         snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
586         log_msg("command: '%s'\n", command);
587
588         if (ctx->continuous_mode) {
589                 if (fork() != 0) {
590                         /* Parent process or error in fork() */
591                         if (urilen > 0)
592                                 free(quoted_uri);
593                         free(command);  
594                         return;
595                 }
596                 /* Child process */
597                 setsid();
598                 close_stdio();
599         }
600         execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
601 }
602
603
604 /* The list of known browsers and how to launch them */
605 static struct browser_launcher browser_launchers[] = {
606         { "microb", launch_microb, NULL, NULL }, /* First entry is the default! */
607         { "tear", launch_tear, NULL, "/usr/bin/tear" },
608         { "fennec", NULL, "fennec %s", "/usr/bin/fennec" },
609         { "opera", NULL, "opera %s", "/usr/bin/opera" },
610         { "midori", NULL, "midori %s", "/usr/bin/midori" },
611         { NULL, NULL, NULL, NULL },
612 };
613
614 static void use_launcher_as_default(struct swb_context *ctx,
615                                     struct browser_launcher *browser) {
616         if (!ctx || !browser)
617                 return;
618
619         if (browser->launcher)
620                 ctx->default_browser_launcher = browser->launcher;
621         else if (browser->other_browser_cmd) {
622                 free(ctx->other_browser_cmd);
623
624                 /* Make a copy of the string constant so that
625                    ctx->other_browser_cmd is safe to free() */
626                 ctx->other_browser_cmd = strdup(browser->other_browser_cmd);
627                 if (!ctx->other_browser_cmd) {
628                         log_msg("malloc failed!\n");
629                         /* Ideally, we'd configure the built-in default here --
630                            but it's possible we could be called in that path */
631                         exit(1);
632                 } else
633                         ctx->default_browser_launcher = launch_other_browser;
634         }
635
636         return;
637 }
638
639 void update_default_browser(struct swb_context *ctx, char *default_browser) {
640         struct browser_launcher *browser;
641
642         if (!ctx)
643                 return;
644
645         /* Configure the built-in default to start -- that way, we can
646            handle errors by just returning */
647         use_launcher_as_default(ctx, &browser_launchers[0]);
648
649         if (!default_browser)
650                 /* No default_browser configured -- use built-in default */
651                 return;
652
653         /* Go through the list of known browser launchers and use one if
654            it matches */
655         for (browser = browser_launchers; browser->name; ++browser)
656                 if (!strcmp(default_browser, browser->name)) {
657                         /* Make sure the user's choice is installed on the
658                            system */
659                         if (browser->binary && access(browser->binary, X_OK)) {
660                                 log_msg("%s appears not to be installed\n",
661                                         default_browser);
662                         } else {
663                                 use_launcher_as_default(ctx, browser);
664                                 return;
665                         }
666                 }
667
668         /* Deal with default_browser = "other" */
669         if (!strcmp(default_browser, "other")) {
670                 if (ctx->other_browser_cmd)
671                         ctx->default_browser_launcher = launch_other_browser;
672                 else
673                         log_msg("default_browser is 'other', but no other_browser_cmd set -- using default\n");
674                 return;
675         }
676
677         /* Unknown value of default_browser */
678         log_msg("Unknown default_browser %s, using default\n", default_browser);
679         return;
680 }
681
682 void launch_browser(struct swb_context *ctx, char *uri) {
683         if (ctx && ctx->default_browser_launcher)
684                 ctx->default_browser_launcher(ctx, uri);
685 }