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