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