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