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