819977ee95b2be61a6b7d1814ba6b66fbb0e3377
[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         if ((pid = fork()) == -1) {
208                 perror("fork");
209                 exit(1);
210         }
211 #ifdef FREMANTLE
212         /* Put together the path to the MicroB browserd lockfile */
213         if (!(homedir = getenv("HOME")))
214                 homedir = DEFAULT_HOMEDIR;
215         len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
216         if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
217                 printf("calloc() failed\n");
218                 exit(1);
219         }
220         snprintf(microb_profile_dir, len, "%s%s",
221                  homedir, MICROB_PROFILE_DIR);
222         len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
223               strlen("/") + strlen(MICROB_LOCKFILE) + 1;
224         if (!(microb_lockfile = calloc(len, sizeof(char)))) {
225                 printf("calloc() failed\n");
226                 exit(1);
227         }
228         snprintf(microb_lockfile, len, "%s%s/%s",
229                  homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
230
231         /* Watch for the creation of a MicroB browserd lockfile
232            NB: The watch has to be set up here, before the browser
233            is launched, to make sure there's no race between browserd
234            starting and us creating the watch */
235         if ((fd = inotify_init()) == -1) {
236                 perror("inotify_init");
237                 exit(1);
238         }
239         if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
240                                          IN_CREATE)) == -1) {
241                 perror("inotify_add_watch");
242                 exit(1);
243         }
244         free(microb_profile_dir);
245
246         if (pid > 0) {
247                 /* Parent process */
248                 /* Wait for our child to start the browser UI process and
249                    for it to acquire the com.nokia.osso_browser D-Bus name,
250                    then make the appropriate method call to open the browser
251                    window.
252
253                    Ideas for how to do this monitoring derived from the
254                    dbus-monitor code (tools/dbus-monitor.c in the D-Bus
255                    codebase). */
256                 microb_started = 0;
257                 dbus_error_init(&dbus_error);
258
259                 raw_connection = dbus_bus_get_private(DBUS_BUS_SESSION,
260                                                       &dbus_error);
261                 if (!raw_connection) {
262                         fprintf(stderr,
263                                 "Failed to open connection to session bus: %s\n",
264                                 dbus_error.message);
265                         dbus_error_free(&dbus_error);
266                         exit(1);
267                 }
268
269                 dbus_bus_add_match(raw_connection,
270                                    "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
271                                    &dbus_error);
272                 if (dbus_error_is_set(&dbus_error)) {
273                         fprintf(stderr,
274                                 "Failed to set up watch for browser UI start: %s\n",
275                                 dbus_error.message);
276                         dbus_error_free(&dbus_error);
277                         exit(1);
278                 }
279                 filter_func = check_microb_started;
280                 if (!dbus_connection_add_filter(raw_connection,
281                                                 filter_func, NULL, NULL)) {
282                         fprintf(stderr, "Failed to set up watch filter!\n");
283                         exit(1);
284                 }
285                 printf("Waiting for MicroB to start\n");
286                 while (!microb_started &&
287                        dbus_connection_read_write_dispatch(raw_connection,
288                                                            -1));
289                 dbus_connection_remove_filter(raw_connection,
290                                               filter_func, NULL);
291                 dbus_bus_remove_match(raw_connection,
292                                       "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
293                                       &dbus_error);
294                 if (dbus_error_is_set(&dbus_error))
295                         /* Don't really care -- about to disconnect from the
296                            bus anyhow */
297                         dbus_error_free(&dbus_error);
298                 dbus_connection_close(raw_connection);
299                 dbus_connection_unref(raw_connection);
300
301                 /* Browser UI's started, send it the request for a new window
302                    via D-Bus */
303                 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
304                                 "com.nokia.osso_browser",
305                                 "/com/nokia/osso_browser/request",
306                                 "com.nokia.osso_browser");
307                 if (!g_proxy) {
308                         printf("Couldn't get a com.nokia.osso_browser proxy\n");
309                         exit(1);
310                 }
311                 if (!strcmp(uri, "new_window")) {
312 #if 0 /* Since we can't detect when the bookmark window closes, we'd have a
313          corner case where, if the user just closes the bookmark window
314          without opening any browser windows, we don't kill off MicroB or
315          resume handling com.nokia.osso_browser */
316                         if (!dbus_g_proxy_call(g_proxy, "top_application",
317                                                &gerror, G_TYPE_INVALID,
318                                                G_TYPE_INVALID)) {
319                                 printf("Opening window failed: %s\n",
320                                        gerror->message);
321                                 exit(1);
322                         }
323 #endif
324                         if (!dbus_g_proxy_call(g_proxy, "load_url",
325                                                &gerror,
326                                                G_TYPE_STRING, "about:blank",
327                                                G_TYPE_INVALID,
328                                                G_TYPE_INVALID)) {
329                                 printf("Opening window failed: %s\n",
330                                        gerror->message);
331                                 exit(1);
332                         }
333                 } else {
334                         if (!dbus_g_proxy_call(g_proxy, "load_url",
335                                                &gerror,
336                                                G_TYPE_STRING, uri,
337                                                G_TYPE_INVALID,
338                                                G_TYPE_INVALID)) {
339                                 printf("Opening window failed: %s\n",
340                                        gerror->message);
341                                 exit(1);
342                         }
343                 }
344                 g_object_unref(g_proxy);
345
346                 /* Workaround: the browser process we started is going to want
347                    to hang around forever, hogging the com.nokia.osso_browser
348                    D-Bus interface while at it.  To fix this, we notice that
349                    when the last browser window closes, the browser UI restarts
350                    its attached browserd process.  Get the browserd process's
351                    PID and use ptrace() to watch for process termination.
352
353                    This has the problem of not being able to detect whether
354                    the bookmark window is open and/or in use, but it's the best
355                    that I can think of.  Better suggestions would be greatly
356                    appreciated. */
357
358                 /* Wait for the MicroB browserd lockfile to be created */
359                 printf("Waiting for browserd lockfile to be created\n");
360                 memset(buf, '\0', 256);
361                 /* read() blocks until there are events to be read */
362                 while ((bytes_read = read(fd, buf, 255)) > 0) {
363                         pos = buf;
364                         /* Loop until we see the event we're looking for
365                            or until all the events are processed */
366                         while (pos && (pos-buf) < bytes_read) {
367                                 event = (struct inotify_event *)pos;
368                                 len = sizeof(struct inotify_event)
369                                       + event->len;
370                                 if (!strcmp(MICROB_LOCKFILE,
371                                             event->name)) {
372                                         /* Lockfile created */
373                                         pos = NULL;
374                                         break;
375                                 } else if ((pos-buf) + len < bytes_read)
376                                         /* More events to process */
377                                         pos += len;
378                                 else
379                                         /* All events processed */
380                                         pos = buf + bytes_read;
381                         }
382                         if (!pos)
383                                 /* Event found, stop looking */
384                                 break;
385                         memset(buf, '\0', 256);
386                 }
387                 inotify_rm_watch(fd, inot_wd);
388                 close(fd);
389
390                 /* Get the PID of the browserd from the lockfile */
391                 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
392                         if (browserd_pid == 0)
393                                 printf("Profile lockfile link lacks PID\n");
394                         else
395                                 printf("readlink() on lockfile failed: %s\n",
396                                        strerror(-browserd_pid));
397                         exit(1);
398                 }
399                 free(microb_lockfile);
400
401                 /* Wait for the browserd to close */
402                 printf("Waiting for MicroB (browserd pid %d) to finish\n",
403                        browserd_pid);
404                 /* Clear any existing SIGCHLD handler to prevent interference
405                    with our wait() */
406                 act.sa_handler = SIG_DFL;
407                 act.sa_flags = 0;
408                 sigemptyset(&(act.sa_mask));
409                 if (sigaction(SIGCHLD, &act, &oldact) == -1) {
410                         perror("clearing SIGCHLD handler failed");
411                         exit(1);
412                 }
413
414                 /* Trace the browserd to get a close notification */
415                 ignore_sigstop = 1;
416                 if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
417                         perror("PTRACE_ATTACH");
418                         exit(1);
419                 }
420                 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
421                 while ((waited_pid = wait(&status)) > 0) {
422                         if (waited_pid != browserd_pid)
423                                 /* Not interested in other processes */
424                                 continue;
425                         if (WIFEXITED(status) || WIFSIGNALED(status))
426                                 /* browserd exited */
427                                 break;
428                         else if (WIFSTOPPED(status)) {
429                                 /* browserd was sent a signal
430                                    We're responsible for making sure this
431                                    signal gets delivered */
432                                 if (ignore_sigstop &&
433                                     WSTOPSIG(status) == SIGSTOP) {
434                                         /* Ignore the first SIGSTOP received
435                                            This is raised for some reason
436                                            immediately after we start tracing
437                                            the process, and won't be followed
438                                            by a SIGCONT at any point */
439                                         printf("Ignoring first SIGSTOP\n");
440                                         ptrace(PTRACE_CONT, browserd_pid,
441                                                NULL, NULL);
442                                         ignore_sigstop = 0;
443                                         continue;
444                                 }
445                                 printf("Forwarding signal %d to browserd\n",
446                                        WSTOPSIG(status));
447                                 ptrace(PTRACE_CONT, browserd_pid,
448                                        NULL, WSTOPSIG(status));
449                         }
450                 }
451
452                 /* Kill off browser UI
453                    XXX: There is a race here with the restarting of the closed
454                    browserd; if that happens before we kill the browser UI, the
455                    newly started browserd may not close with the UI
456                    XXX: Hope we don't cause data loss here! */
457                 printf("Killing MicroB\n");
458                 kill(pid, SIGTERM);
459                 waitpid(pid, &status, 0);
460
461                 /* Restore old SIGCHLD handler */
462                 if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
463                         perror("restoring old SIGCHLD handler failed");
464                         exit(1);
465                 }
466         } else {
467                 /* Child process */
468                 close(fd);
469                 close_stdio();
470
471                 /* exec maemo-invoker directly instead of relying on the
472                    /usr/bin/browser symlink, since /usr/bin/browser may have
473                    been replaced with a shell script calling us via D-Bus */
474                 /* Launch the browser in the background -- our parent will
475                    wait for it to claim the D-Bus name and then display the
476                    window using D-Bus */
477                 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
478         }
479 #else /* !FREMANTLE */
480         if (pid > 0) {
481                 /* Parent process */
482                 waitpid(pid, &status, 0);
483         } else {
484                 /* Child process */
485                 close_stdio();
486
487                 /* exec maemo-invoker directly instead of relying on the
488                    /usr/bin/browser symlink, since /usr/bin/browser may have
489                    been replaced with a shell script calling us via D-Bus */
490                 if (!strcmp(uri, "new_window")) {
491                         execl("/usr/bin/maemo-invoker",
492                               "browser", (char *)NULL);
493                 } else {
494                         execl("/usr/bin/maemo-invoker",
495                               "browser", "--url", uri, (char *)NULL);
496                 }
497         }
498 #endif /* FREMANTLE */
499
500         /* Kill off browserd if we started it */
501         if (kill_browserd)
502                 system("kill `pidof browserd`");
503
504         if (!ctx || !ctx->continuous_mode) 
505                 exit(0);
506
507         dbus_request_osso_browser_name(ctx);
508 }
509
510 static void launch_other_browser(struct swb_context *ctx, char *uri) {
511         char *command;
512         char *quoted_uri, *quote;
513
514         size_t cmdlen, urilen;
515         size_t quoted_uri_size;
516         size_t offset;
517
518         if (!uri || !strcmp(uri, "new_window"))
519                 uri = "";
520
521         printf("launch_other_browser with uri '%s'\n", uri);
522
523         if ((urilen = strlen(uri)) > 0) {
524                 /* Quote the URI to prevent the shell from interpreting it */
525                 /* urilen+3 = length of URI + 2x \' + \0 */
526                 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
527                         exit(1);
528                 snprintf(quoted_uri, urilen+3, "'%s'", uri);
529
530                 /* If there are any 's in the original URI, URL-escape them
531                    (replace them with %27) */
532                 quoted_uri_size = urilen + 3;
533                 quote = quoted_uri + 1;
534                 while ((quote = strchr(quote, '\'')) &&
535                        (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
536                         /* Check to make sure we don't shrink the memory area
537                            as a result of integer overflow */
538                         if (quoted_uri_size+2 <= quoted_uri_size)
539                                 exit(1);
540
541                         /* Grow the memory area;
542                            2 = strlen("%27")-strlen("'") */
543                         if (!(quoted_uri = realloc(quoted_uri,
544                                                    quoted_uri_size+2)))
545                                 exit(1);
546                         quoted_uri_size = quoted_uri_size + 2;
547
548                         /* Recalculate the location of the ' character --
549                            realloc() may have moved the string in memory */
550                         quote = quoted_uri + offset;
551
552                         /* Move the string after the ', including the \0,
553                            over two chars */
554                         memmove(quote+3, quote+1, strlen(quote));
555                         memcpy(quote, "%27", 3);
556                         quote = quote + 3;
557                 }
558                 urilen = strlen(quoted_uri);
559         } else
560                 quoted_uri = uri;
561
562         cmdlen = strlen(ctx->other_browser_cmd);
563
564         /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
565            replace "%s"), but is needed in the case other_browser_cmd has no %s
566            and urilen < 2 */
567         if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
568                 exit(1);
569         snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
570         printf("command: '%s'\n", command);
571
572         if (ctx->continuous_mode) {
573                 if (fork() != 0) {
574                         /* Parent process or error in fork() */
575                         if (urilen > 0)
576                                 free(quoted_uri);
577                         free(command);  
578                         return;
579                 }
580                 /* Child process */
581                 setsid();
582                 close_stdio();
583         }
584         execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
585 }
586
587 /* Use launch_other_browser as the default browser launcher, with the string
588    passed in as the other_browser_cmd
589    Resulting other_browser_cmd is always safe to free(), even if a pointer
590    to a string constant is passed in */
591 static void use_other_browser_cmd(struct swb_context *ctx, char *cmd) {
592         size_t len = strlen(cmd);
593
594         free(ctx->other_browser_cmd);
595         ctx->other_browser_cmd = calloc(len+1, sizeof(char));
596         if (!ctx->other_browser_cmd) {
597                 printf("malloc failed!\n");
598                 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
599         } else {
600                 ctx->other_browser_cmd = strncpy(ctx->other_browser_cmd,
601                                                  cmd, len+1);
602                 ctx->default_browser_launcher = launch_other_browser;
603         }
604 }
605
606 void update_default_browser(struct swb_context *ctx, char *default_browser) {
607         if (!ctx)
608                 return;
609
610         if (!default_browser) {
611                 /* No default_browser configured -- use built-in default */
612                 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
613                 return;
614         }
615
616         if (!strcmp(default_browser, "tear"))
617                 ctx->default_browser_launcher = launch_tear;
618         else if (!strcmp(default_browser, "microb"))
619                 ctx->default_browser_launcher = launch_microb;
620         else if (!strcmp(default_browser, "fennec"))
621                 /* Cheat and reuse launch_other_browser, since we don't appear
622                    to need to do anything special */
623                 use_other_browser_cmd(ctx, "fennec %s");
624         else if (!strcmp(default_browser, "midori"))
625                 use_other_browser_cmd(ctx, "midori %s");
626         else if (!strcmp(default_browser, "other")) {
627                 if (ctx->other_browser_cmd)
628                         ctx->default_browser_launcher = launch_other_browser;
629                 else {
630                         printf("default_browser is 'other', but no other_browser_cmd set -- using default\n");
631                         ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
632                 }
633         } else {
634                 printf("Unknown default_browser %s, using default", default_browser);
635                 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
636         }
637 }
638
639 void launch_browser(struct swb_context *ctx, char *uri) {
640         if (ctx && ctx->default_browser_launcher)
641                 ctx->default_browser_launcher(ctx, uri);
642 }