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 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                         tear_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
135                                         "com.nokia.tear", "/com/nokia/tear",
136                                         "com.nokia.Tear");
137                 dbus_g_proxy_call(tear_proxy, "OpenAddress", &error,
138                                   G_TYPE_STRING, uri, G_TYPE_INVALID);
139                 if (!ctx->continuous_mode)
140                         exit(0);
141         } else {
142                 if (ctx->continuous_mode) {
143                         if ((pid = fork()) != 0) {
144                                 /* Parent process or error in fork() */
145                                 printf("child: %d\n", (int)pid);
146                                 return;
147                         }
148                         /* Child process */
149                         setsid();
150                         close_stdio();
151                 }
152                 execl("/usr/bin/tear", "/usr/bin/tear", uri, (char *)NULL);
153         }
154 }
155
156 void launch_microb(struct swb_context *ctx, char *uri) {
157         int kill_browserd = 0;
158         int status;
159         pid_t pid;
160 #ifdef FREMANTLE
161         char *homedir, *microb_profile_dir, *microb_lockfile;
162         size_t len;
163         int fd, inot_wd;
164         DBusConnection *raw_connection;
165         DBusError dbus_error;
166         DBusHandleMessageFunction filter_func;
167         DBusGProxy *g_proxy;
168         GError *gerror = NULL;
169         int bytes_read;
170         char buf[256], *pos;
171         struct inotify_event *event;
172         pid_t browserd_pid, waited_pid;
173         struct sigaction act, oldact;
174         int ignore_sigstop;
175 #endif
176
177         if (!uri)
178                 uri = "new_window";
179
180         printf("launch_microb with uri '%s'\n", uri);
181
182         /* Launch browserd if it's not running */
183         status = system("pidof browserd > /dev/null");
184         if (WIFEXITED(status) && WEXITSTATUS(status)) {
185                 kill_browserd = 1;
186 #ifdef FREMANTLE
187                 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
188 #else
189                 system("/usr/sbin/browserd -d > /dev/null 2>&1");
190 #endif
191         }
192
193         /* Release the osso_browser D-Bus name so that MicroB can take it */
194         dbus_release_osso_browser_name(ctx);
195
196 #ifdef FREMANTLE
197         /* Put together the path to the MicroB browserd lockfile */
198         if (!(homedir = getenv("HOME")))
199                 homedir = DEFAULT_HOMEDIR;
200         len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) + 1;
201         if (!(microb_profile_dir = calloc(len, sizeof(char)))) {
202                 printf("calloc() failed\n");
203                 exit(1);
204         }
205         snprintf(microb_profile_dir, len, "%s%s",
206                  homedir, MICROB_PROFILE_DIR);
207         len = strlen(homedir) + strlen(MICROB_PROFILE_DIR) +
208               strlen("/") + strlen(MICROB_LOCKFILE) + 1;
209         if (!(microb_lockfile = calloc(len, sizeof(char)))) {
210                 printf("calloc() failed\n");
211                 exit(1);
212         }
213         snprintf(microb_lockfile, len, "%s%s/%s",
214                  homedir, MICROB_PROFILE_DIR, MICROB_LOCKFILE);
215
216         /* Watch for the creation of a MicroB browserd lockfile
217            NB: The watch has to be set up here, before the browser
218            is launched, to make sure there's no race between browserd
219            starting and us creating the watch */
220         if ((fd = inotify_init()) == -1) {
221                 perror("inotify_init");
222                 exit(1);
223         }
224         if ((inot_wd = inotify_add_watch(fd, microb_profile_dir,
225                                          IN_CREATE)) == -1) {
226                 perror("inotify_add_watch");
227                 exit(1);
228         }
229         free(microb_profile_dir);
230
231         /* Set up the D-Bus eavesdropping we'll use to watch for MicroB
232            acquiring the com.nokia.osso_browser D-Bus name.  Again, this needs
233            to happen before the browser is launched, so that there's no race
234            between establishing the watch and browser startup.
235
236            Ideas for how to do this monitoring derived from the dbus-monitor
237            code (tools/dbus-monitor.c in the D-Bus codebase). */
238         dbus_error_init(&dbus_error);
239
240         raw_connection = dbus_bus_get_private(DBUS_BUS_SESSION, &dbus_error);
241         if (!raw_connection) {
242                 fprintf(stderr,
243                         "Failed to open connection to session bus: %s\n",
244                         dbus_error.message);
245                 dbus_error_free(&dbus_error);
246                 exit(1);
247         }
248
249         dbus_bus_add_match(raw_connection,
250                            "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
251                            &dbus_error);
252         if (dbus_error_is_set(&dbus_error)) {
253                 fprintf(stderr,
254                         "Failed to set up watch for browser UI start: %s\n",
255                         dbus_error.message);
256                 dbus_error_free(&dbus_error);
257                 exit(1);
258         }
259         filter_func = check_microb_started;
260         if (!dbus_connection_add_filter(raw_connection,
261                                         filter_func, NULL, NULL)) {
262                 fprintf(stderr, "Failed to set up watch filter!\n");
263                 exit(1);
264         }
265
266         if ((pid = fork()) == -1) {
267                 perror("fork");
268                 exit(1);
269         }
270
271         if (pid > 0) {
272                 /* Parent process */
273                 /* Wait for our child to start the browser UI process and
274                    for it to acquire the com.nokia.osso_browser D-Bus name,
275                    then make the appropriate method call to open the browser
276                    window. */
277                 microb_started = 0;
278                 printf("Waiting for MicroB to start\n");
279                 while (!microb_started &&
280                        dbus_connection_read_write_dispatch(raw_connection,
281                                                            -1));
282                 dbus_connection_remove_filter(raw_connection,
283                                               filter_func, NULL);
284                 dbus_bus_remove_match(raw_connection,
285                                       "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
286                                       &dbus_error);
287                 if (dbus_error_is_set(&dbus_error))
288                         /* Don't really care -- about to disconnect from the
289                            bus anyhow */
290                         dbus_error_free(&dbus_error);
291                 dbus_connection_close(raw_connection);
292                 dbus_connection_unref(raw_connection);
293
294                 /* Browser UI's started, send it the request for a new window
295                    via D-Bus */
296                 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
297                                 "com.nokia.osso_browser",
298                                 "/com/nokia/osso_browser/request",
299                                 "com.nokia.osso_browser");
300                 if (!g_proxy) {
301                         printf("Couldn't get a com.nokia.osso_browser proxy\n");
302                         exit(1);
303                 }
304                 if (!strcmp(uri, "new_window")) {
305 #if 0 /* Since we can't detect when the bookmark window closes, we'd have a
306          corner case where, if the user just closes the bookmark window
307          without opening any browser windows, we don't kill off MicroB or
308          resume handling com.nokia.osso_browser */
309                         if (!dbus_g_proxy_call(g_proxy, "top_application",
310                                                &gerror, G_TYPE_INVALID,
311                                                G_TYPE_INVALID)) {
312                                 printf("Opening window failed: %s\n",
313                                        gerror->message);
314                                 exit(1);
315                         }
316 #endif
317                         if (!dbus_g_proxy_call(g_proxy, "load_url",
318                                                &gerror,
319                                                G_TYPE_STRING, "about:blank",
320                                                G_TYPE_INVALID,
321                                                G_TYPE_INVALID)) {
322                                 printf("Opening window failed: %s\n",
323                                        gerror->message);
324                                 exit(1);
325                         }
326                 } else {
327                         if (!dbus_g_proxy_call(g_proxy, "load_url",
328                                                &gerror,
329                                                G_TYPE_STRING, uri,
330                                                G_TYPE_INVALID,
331                                                G_TYPE_INVALID)) {
332                                 printf("Opening window failed: %s\n",
333                                        gerror->message);
334                                 exit(1);
335                         }
336                 }
337                 g_object_unref(g_proxy);
338
339                 /* Workaround: the browser process we started is going to want
340                    to hang around forever, hogging the com.nokia.osso_browser
341                    D-Bus interface while at it.  To fix this, we notice that
342                    when the last browser window closes, the browser UI restarts
343                    its attached browserd process.  Get the browserd process's
344                    PID and use ptrace() to watch for process termination.
345
346                    This has the problem of not being able to detect whether
347                    the bookmark window is open and/or in use, but it's the best
348                    that I can think of.  Better suggestions would be greatly
349                    appreciated. */
350
351                 /* Wait for the MicroB browserd lockfile to be created */
352                 printf("Waiting for browserd lockfile to be created\n");
353                 memset(buf, '\0', 256);
354                 /* read() blocks until there are events to be read */
355                 while ((bytes_read = read(fd, buf, 255)) > 0) {
356                         pos = buf;
357                         /* Loop until we see the event we're looking for
358                            or until all the events are processed */
359                         while (pos && (pos-buf) < bytes_read) {
360                                 event = (struct inotify_event *)pos;
361                                 len = sizeof(struct inotify_event)
362                                       + event->len;
363                                 if (!strcmp(MICROB_LOCKFILE,
364                                             event->name)) {
365                                         /* Lockfile created */
366                                         pos = NULL;
367                                         break;
368                                 } else if ((pos-buf) + len < bytes_read)
369                                         /* More events to process */
370                                         pos += len;
371                                 else
372                                         /* All events processed */
373                                         pos = buf + bytes_read;
374                         }
375                         if (!pos)
376                                 /* Event found, stop looking */
377                                 break;
378                         memset(buf, '\0', 256);
379                 }
380                 inotify_rm_watch(fd, inot_wd);
381                 close(fd);
382
383                 /* Get the PID of the browserd from the lockfile */
384                 if ((browserd_pid = get_browserd_pid(microb_lockfile)) <= 0) {
385                         if (browserd_pid == 0)
386                                 printf("Profile lockfile link lacks PID\n");
387                         else
388                                 printf("readlink() on lockfile failed: %s\n",
389                                        strerror(-browserd_pid));
390                         exit(1);
391                 }
392                 free(microb_lockfile);
393
394                 /* Wait for the browserd to close */
395                 printf("Waiting for MicroB (browserd pid %d) to finish\n",
396                        browserd_pid);
397                 /* Clear any existing SIGCHLD handler to prevent interference
398                    with our wait() */
399                 act.sa_handler = SIG_DFL;
400                 act.sa_flags = 0;
401                 sigemptyset(&(act.sa_mask));
402                 if (sigaction(SIGCHLD, &act, &oldact) == -1) {
403                         perror("clearing SIGCHLD handler failed");
404                         exit(1);
405                 }
406
407                 /* Trace the browserd to get a close notification */
408                 ignore_sigstop = 1;
409                 if (ptrace(PTRACE_ATTACH, browserd_pid, NULL, NULL) == -1) {
410                         perror("PTRACE_ATTACH");
411                         exit(1);
412                 }
413                 ptrace(PTRACE_CONT, browserd_pid, NULL, NULL);
414                 while ((waited_pid = wait(&status)) > 0) {
415                         if (waited_pid != browserd_pid)
416                                 /* Not interested in other processes */
417                                 continue;
418                         if (WIFEXITED(status) || WIFSIGNALED(status))
419                                 /* browserd exited */
420                                 break;
421                         else if (WIFSTOPPED(status)) {
422                                 /* browserd was sent a signal
423                                    We're responsible for making sure this
424                                    signal gets delivered */
425                                 if (ignore_sigstop &&
426                                     WSTOPSIG(status) == SIGSTOP) {
427                                         /* Ignore the first SIGSTOP received
428                                            This is raised for some reason
429                                            immediately after we start tracing
430                                            the process, and won't be followed
431                                            by a SIGCONT at any point */
432                                         printf("Ignoring first SIGSTOP\n");
433                                         ptrace(PTRACE_CONT, browserd_pid,
434                                                NULL, NULL);
435                                         ignore_sigstop = 0;
436                                         continue;
437                                 }
438                                 printf("Forwarding signal %d to browserd\n",
439                                        WSTOPSIG(status));
440                                 ptrace(PTRACE_CONT, browserd_pid,
441                                        NULL, WSTOPSIG(status));
442                         }
443                 }
444
445                 /* Kill off browser UI
446                    XXX: There is a race here with the restarting of the closed
447                    browserd; if that happens before we kill the browser UI, the
448                    newly started browserd may not close with the UI
449                    XXX: Hope we don't cause data loss here! */
450                 printf("Killing MicroB\n");
451                 kill(pid, SIGTERM);
452                 waitpid(pid, &status, 0);
453
454                 /* Restore old SIGCHLD handler */
455                 if (sigaction(SIGCHLD, &oldact, NULL) == -1) {
456                         perror("restoring old SIGCHLD handler failed");
457                         exit(1);
458                 }
459         } else {
460                 /* Child process */
461                 dbus_connection_close(raw_connection);
462                 dbus_connection_unref(raw_connection);
463                 close(fd);
464                 close_stdio();
465
466                 /* exec maemo-invoker directly instead of relying on the
467                    /usr/bin/browser symlink, since /usr/bin/browser may have
468                    been replaced with a shell script calling us via D-Bus */
469                 /* Launch the browser in the background -- our parent will
470                    wait for it to claim the D-Bus name and then display the
471                    window using D-Bus */
472                 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
473         }
474 #else /* !FREMANTLE */
475         if ((pid = fork()) == -1) {
476                 perror("fork");
477                 exit(1);
478         }
479
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 }