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