Ensure that only one browser-switchboard is active at any time
[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 <unistd.h>
27 #include <sys/types.h>
28 #include <sys/wait.h>
29 #include <sys/stat.h>
30 #include <fcntl.h>
31 #include <dbus/dbus-glib.h>
32
33 #ifdef FREMANTLE
34 #include <dbus/dbus.h>
35 #include <errno.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
49 #define LAUNCH_DEFAULT_BROWSER launch_microb
50
51 #ifdef FREMANTLE
52 static int microb_started = 0;
53
54 /* Check to see whether MicroB is ready to handle D-Bus requests yet
55    See the comments in launch_microb to understand how this works. */
56 static DBusHandlerResult check_microb_started(DBusConnection *connection,
57                                      DBusMessage *message,
58                                      void *user_data) {
59         DBusError error;
60         char *name, *old, *new;
61
62         printf("Checking to see if MicroB is ready\n");
63         dbus_error_init(&error);
64         if (!dbus_message_get_args(message, &error,
65                                    DBUS_TYPE_STRING, &name,
66                                    DBUS_TYPE_STRING, &old,
67                                    DBUS_TYPE_STRING, &new,
68                                    DBUS_TYPE_INVALID)) {
69                 printf("%s\n", error.message);
70                 dbus_error_free(&error);
71                 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
72         }
73         /* If old is an empty string, then the name has been acquired, and
74            MicroB should be ready to handle our request */
75         if (strlen(old) == 0) {
76                 printf("MicroB ready\n");
77                 microb_started = 1;
78         }
79
80         return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
81 }
82
83 /* Get a browserd PID from the corresponding Mozilla profile lockfile */
84 static pid_t get_browserd_pid(const char *lockfile) {
85         char buf[256], *tmp;
86
87         /* The lockfile is a symlink pointing to "[ipaddr]:+[pid]", so read in
88            the target of the symlink and parse it that way */
89         memset(buf, '\0', 256);
90         if (readlink(lockfile, buf, 255) == -1)
91                 return -errno;
92         if (!(tmp = strstr(buf, ":+")))
93                 return 0;
94         tmp += 2; /* Skip over the ":+" */
95
96         return atoi(tmp);
97 }
98 #endif
99
100 /* Close stdin/stdout/stderr and replace with /dev/null */
101 static int close_stdio(void) {
102         int fd;
103
104         if ((fd = open("/dev/null", O_RDWR)) == -1)
105                 return -1;
106
107         if (dup2(fd, 0) == -1 || dup2(fd, 1) == -1 || dup2(fd, 2) == -1)
108                 return -1;
109
110         close(fd);
111         return 0;
112 }
113
114 static void launch_tear(struct swb_context *ctx, char *uri) {
115         int status;
116         static DBusGProxy *tear_proxy = NULL;
117         GError *error = NULL;
118         pid_t pid;
119
120         if (!uri)
121                 uri = "new_window";
122
123         printf("launch_tear with uri '%s'\n", uri);
124
125         /* We should be able to just call the D-Bus service to open Tear ...
126            but if Tear's not open, that cuases D-Bus to start Tear and then
127            pass it the OpenAddress call, which results in two browser windows.
128            Properly fixing this probably requires Tear to provide a D-Bus
129            method that opens an address in an existing window, but for now work
130            around by just invoking Tear with exec() if it's not running. */
131         status = system("pidof tear > /dev/null");
132         if (WIFEXITED(status) && !WEXITSTATUS(status)) {
133                 if (!tear_proxy) {
134                         if (!(tear_proxy = dbus_g_proxy_new_for_name(
135                                                 ctx->session_bus,
136                                                 "com.nokia.tear",
137                                                 "/com/nokia/tear",
138                                                 "com.nokia.Tear"))) {
139                                 printf("Failed to create proxy for com.nokia.Tear D-Bus interface\n");
140                                 exit(1);
141                         }
142                 }
143
144                 if (!dbus_g_proxy_call(tear_proxy, "OpenAddress", &error,
145                                        G_TYPE_STRING, uri, G_TYPE_INVALID,
146                                        G_TYPE_INVALID)) {
147                         printf("Opening window failed: %s\n", error->message);
148                         exit(1);
149                 }
150                 if (!ctx->continuous_mode)
151                         exit(0);
152         } else {
153                 if (ctx->continuous_mode) {
154                         if ((pid = fork()) != 0) {
155                                 /* Parent process or error in fork() */
156                                 printf("child: %d\n", (int)pid);
157                                 return;
158                         }
159                         /* Child process */
160                         setsid();
161                         close_stdio();
162                 }
163                 execl("/usr/bin/tear", "/usr/bin/tear", uri, (char *)NULL);
164         }
165 }
166
167 void launch_microb(struct swb_context *ctx, char *uri) {
168         int kill_browserd = 0;
169         int status;
170         pid_t pid;
171 #ifdef FREMANTLE
172         char *homedir, *microb_profile_dir, *microb_lockfile;
173         size_t len;
174         int fd, inot_wd;
175         DBusConnection *raw_connection;
176         DBusError dbus_error;
177         DBusHandleMessageFunction filter_func;
178         DBusGProxy *g_proxy;
179         GError *gerror = NULL;
180         int bytes_read;
181         char buf[256], *pos;
182         struct inotify_event *event;
183         pid_t browserd_pid, waited_pid;
184         struct sigaction act, oldact;
185         int ignore_sigstop;
186 #endif
187
188         if (!uri)
189                 uri = "new_window";
190
191         printf("launch_microb with uri '%s'\n", uri);
192
193         /* Launch browserd if it's not running */
194         status = system("pidof browserd > /dev/null");
195         if (WIFEXITED(status) && WEXITSTATUS(status)) {
196                 kill_browserd = 1;
197 #ifdef FREMANTLE
198                 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
199 #else
200                 system("/usr/sbin/browserd -d > /dev/null 2>&1");
201 #endif
202         }
203
204         /* Release the osso_browser D-Bus name so that MicroB can take it */
205         dbus_release_osso_browser_name(ctx);
206
207 #ifdef FREMANTLE
208         /* Put together the path to the MicroB browserd lockfile */
209         if (!(homedir = getenv("HOME")))
210                 homedir = DEFAULT_HOMEDIR;
211         len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
212         if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
213                 printf("calloc() failed\n");
214                 exit(1);
215         }
216         snprintf(microb_profile_dir, len, "%s%s",
217                  homedir, MICROB_PROFILE_DIR);
218         len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
219               strlen("/") + strlen(MICROB_LOCKFILE) + 1;
220         if (!(microb_lockfile = calloc(len, sizeof(char)))) {
221                 printf("calloc() failed\n");
222                 exit(1);
223         }
224         snprintf(microb_lockfile, len, "%s%s/%s",
225                  homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
226
227         /* Watch for the creation of a MicroB browserd lockfile
228            NB: The watch has to be set up here, before the browser
229            is launched, to make sure there's no race between browserd
230            starting and us creating the watch */
231         if ((fd = inotify_init()) == -1) {
232                 perror("inotify_init");
233                 exit(1);
234         }
235         if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
236                                          IN_CREATE)) == -1) {
237                 perror("inotify_add_watch");
238                 exit(1);
239         }
240         free(microb_profile_dir);
241
242         /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
243            acquiring the com.nokia.osso_browser D-Bus name.  Again, this needs
244            to happen before the browser is launched, so that there's no race
245            between establishing the watch and browser startup.
246
247            Ideas for how to do this monitoring derived from the dbus-monitor
248            code (tools/dbus-monitor.c in the D-Bus codebase). */
249         dbus_error_init(&dbus_error);
250
251         raw_connection = dbus_bus_get_private(DBUS_BUS_SESSION, &dbus_error);
252         if (!raw_connection) {
253                 fprintf(stderr,
254                         "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                 fprintf(stderr,
265                         "Failed to set up watch for browser UI start: %s\n",
266                         dbus_error.message);
267                 dbus_error_free(&dbus_error);
268                 exit(1);
269         }
270         filter_func = check_microb_started;
271         if (!dbus_connection_add_filter(raw_connection,
272                                         filter_func, NULL, NULL)) {
273                 fprintf(stderr, "Failed to set up watch filter!\n");
274                 exit(1);
275         }
276
277         if ((pid = fork()) == -1) {
278                 perror("fork");
279                 exit(1);
280         }
281
282         if (pid > 0) {
283                 /* Parent process */
284                 /* Wait for our child to start the browser UI process and
285                    for it to acquire the com.nokia.osso_browser D-Bus name,
286                    then make the appropriate method call to open the browser
287                    window. */
288                 microb_started = 0;
289                 printf("Waiting for MicroB to start\n");
290                 while (!microb_started &&
291                        dbus_connection_read_write_dispatch(raw_connection,
292                                                            -1));
293                 dbus_connection_remove_filter(raw_connection,
294                                               filter_func, NULL);
295                 dbus_bus_remove_match(raw_connection,
296                                       "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
297                                       &dbus_error);
298                 if (dbus_error_is_set(&dbus_error))
299                         /* Don't really care -- about to disconnect from the
300                            bus anyhow */
301                         dbus_error_free(&dbus_error);
302                 dbus_connection_close(raw_connection);
303                 dbus_connection_unref(raw_connection);
304
305                 /* Browser UI's started, send it the request for a new window
306                    via D-Bus */
307                 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
308                                 "com.nokia.osso_browser",
309                                 "/com/nokia/osso_browser/request",
310                                 "com.nokia.osso_browser");
311                 if (!g_proxy) {
312                         printf("Couldn't get a com.nokia.osso_browser proxy\n");
313                         exit(1);
314                 }
315                 if (!strcmp(uri, "new_window")) {
316 #if 0 /* Since we can't detect when the bookmark window closes, we'd have a
317          corner case where, if the user just closes the bookmark window
318          without opening any browser windows, we don't kill off MicroB or
319          resume handling com.nokia.osso_browser */
320                         if (!dbus_g_proxy_call(g_proxy, "top_application",
321                                                &gerror, G_TYPE_INVALID,
322                                                G_TYPE_INVALID)) {
323                                 printf("Opening window failed: %s\n",
324                                        gerror->message);
325                                 exit(1);
326                         }
327 #endif
328                         if (!dbus_g_proxy_call(g_proxy, "load_url",
329                                                &gerror,
330                                                G_TYPE_STRING, "about:blank",
331                                                G_TYPE_INVALID,
332                                                G_TYPE_INVALID)) {
333                                 printf("Opening window failed: %s\n",
334                                        gerror->message);
335                                 exit(1);
336                         }
337                 } else {
338                         if (!dbus_g_proxy_call(g_proxy, "load_url",
339                                                &gerror,
340                                                G_TYPE_STRING, uri,
341                                                G_TYPE_INVALID,
342                                                G_TYPE_INVALID)) {
343                                 printf("Opening window failed: %s\n",
344                                        gerror->message);
345                                 exit(1);
346                         }
347                 }
348                 g_object_unref(g_proxy);
349
350                 /* Workaround: the browser process we started is going to want
351                    to hang around forever, hogging the com.nokia.osso_browser
352                    D-Bus interface while at it.  To fix this, we notice that
353                    when the last browser window closes, the browser UI restarts
354                    its attached browserd process.  Get the browserd process's
355                    PID and use ptrace() to watch for process termination.
356
357                    This has the problem of not being able to detect whether
358                    the bookmark window is open and/or in use, but it's the best
359                    that I can think of.  Better suggestions would be greatly
360                    appreciated. */
361
362                 /* Wait for the MicroB browserd lockfile to be created */
363                 printf("Waiting for browserd lockfile to be created\n");
364                 memset(buf, '\0', 256);
365                 /* read() blocks until there are events to be read */
366                 while ((bytes_read = read(fd, buf, 255)) > 0) {
367                         pos = buf;
368                         /* Loop until we see the event we're looking for
369                            or until all the events are processed */
370                         while (pos && (pos-buf) < bytes_read) {
371                                 event = (struct inotify_event *)pos;
372                                 len = sizeof(struct inotify_event)
373                                       + event->len;
374                                 if (!strcmp(MICROB_LOCKFILE,
375                                             event->name)) {
376                                         /* Lockfile created */
377                                         pos = NULL;
378                                         break;
379                                 } else if ((pos-buf) + len < bytes_read)
380                                         /* More events to process */
381                                         pos += len;
382                                 else
383                                         /* All events processed */
384                                         pos = buf + bytes_read;
385                         }
386                         if (!pos)
387                                 /* Event found, stop looking */
388                                 break;
389                         memset(buf, '\0', 256);
390                 }
391                 inotify_rm_watch(fd, inot_wd);
392                 close(fd);
393
394                 /* Get the PID of the browserd from the lockfile */
395                 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
396                         if (browserd_pid == 0)
397                                 printf("Profile lockfile link lacks PID\n");
398                         else
399                                 printf("readlink() on lockfile failed: %s\n",
400                                        strerror(-browserd_pid));
401                         exit(1);
402                 }
403                 free(microb_lockfile);
404
405                 /* Wait for the browserd to close */
406                 printf("Waiting for MicroB (browserd pid %d) to finish\n",
407                        browserd_pid);
408                 /* Clear any existing SIGCHLD handler to prevent interference
409                    with our wait() */
410                 act.sa_handler = SIG_DFL;
411                 act.sa_flags = 0;
412                 sigemptyset(&(act.sa_mask));
413                 if (sigaction(SIGCHLD, &act, &oldact) == -1) {
414                         perror("clearing SIGCHLD handler failed");
415                         exit(1);
416                 }
417
418                 /* Trace the browserd to get a close notification */
419                 ignore_sigstop = 1;
420                 if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
421                         perror("PTRACE_ATTACH");
422                         exit(1);
423                 }
424                 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
425                 while ((waited_pid = wait(&status)) > 0) {
426                         if (waited_pid != browserd_pid)
427                                 /* Not interested in other processes */
428                                 continue;
429                         if (WIFEXITED(status) || WIFSIGNALED(status))
430                                 /* browserd exited */
431                                 break;
432                         else if (WIFSTOPPED(status)) {
433                                 /* browserd was sent a signal
434                                    We're responsible for making sure this
435                                    signal gets delivered */
436                                 if (ignore_sigstop &&
437                                     WSTOPSIG(status) == SIGSTOP) {
438                                         /* Ignore the first SIGSTOP received
439                                            This is raised for some reason
440                                            immediately after we start tracing
441                                            the process, and won't be followed
442                                            by a SIGCONT at any point */
443                                         printf("Ignoring first SIGSTOP\n");
444                                         ptrace(PTRACE_CONT, browserd_pid,
445                                                NULL, NULL);
446                                         ignore_sigstop = 0;
447                                         continue;
448                                 }
449                                 printf("Forwarding signal %d to browserd\n",
450                                        WSTOPSIG(status));
451                                 ptrace(PTRACE_CONT, browserd_pid,
452                                        NULL, WSTOPSIG(status));
453                         }
454                 }
455
456                 /* Kill off browser UI
457                    XXX: There is a race here with the restarting of the closed
458                    browserd; if that happens before we kill the browser UI, the
459                    newly started browserd may not close with the UI
460                    XXX: Hope we don't cause data loss here! */
461                 printf("Killing MicroB\n");
462                 kill(pid, SIGTERM);
463                 waitpid(pid, &status, 0);
464
465                 /* Restore old SIGCHLD handler */
466                 if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
467                         perror("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                 perror("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         printf("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         printf("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                 printf("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                         printf("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                 printf("Unknown default_browser %s, using default", default_browser);
646                 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
647         }
648 }
649
650 void launch_browser(struct swb_context *ctx, char *uri) {
651         if (ctx && ctx->default_browser_launcher)
652                 ctx->default_browser_launcher(ctx, uri);
653 }