Revert "Take a different approach to detecting MicroB browser window close"
[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 <signal.h>
36 #endif
37
38 #include "browser-switchboard.h"
39 #include "launcher.h"
40 #include "dbus-server-bindings.h"
41
42 #define LAUNCH_DEFAULT_BROWSER launch_microb
43
44 #ifdef FREMANTLE
45 static int microb_started = 0;
46 static int kill_microb = 0;
47
48 /* Check to see whether MicroB is ready to handle D-Bus requests yet
49    See the comments in launch_microb to understand how this works. */
50 static DBusHandlerResult check_microb_started(DBusConnection *connection,
51                                      DBusMessage *message,
52                                      void *user_data) {
53         DBusError error;
54         char *name, *old, *new;
55
56         printf("Checking to see if MicroB is ready\n");
57         dbus_error_init(&error);
58         if (!dbus_message_get_args(message, &error,
59                                    DBUS_TYPE_STRING, &name,
60                                    DBUS_TYPE_STRING, &old,
61                                    DBUS_TYPE_STRING, &new,
62                                    DBUS_TYPE_INVALID)) {
63                 printf("%s\n", error.message);
64                 dbus_error_free(&error);
65                 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
66         }
67         /* If old is an empty string, then the name has been acquired, and
68            MicroB should be ready to handle our request */
69         if (strlen(old) == 0) {
70                 printf("MicroB ready\n");
71                 microb_started = 1;
72         }
73
74         return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
75 }
76
77 /* Check to see whether the last MicroB window has closed
78    See the comments in launch_microb to understand how this works. */
79 static DBusHandlerResult check_microb_finished(DBusConnection *connection,
80                                      DBusMessage *message,
81                                      void *user_data) {
82         DBusError error;
83         char *name, *old, *new;
84
85         printf("Checking to see if we should kill MicroB\n");
86         /* Check to make sure that the Mozilla.MicroB name is being released,
87            not acquired -- if it's being acquired, we might be seeing an event
88            at MicroB startup, in which case killing the browser isn't
89            appropriate */
90         dbus_error_init(&error);
91         if (!dbus_message_get_args(message, &error,
92                                    DBUS_TYPE_STRING, &name,
93                                    DBUS_TYPE_STRING, &old,
94                                    DBUS_TYPE_STRING, &new,
95                                    DBUS_TYPE_INVALID)) {
96                 printf("%s\n", error.message);
97                 dbus_error_free(&error);
98                 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
99         }
100         /* If old isn't an empty string, the name is being released, and
101            we should now kill MicroB */
102         if (strlen(old) > 0)
103                 kill_microb = 1;
104
105         return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
106 }
107 #endif
108
109 /* Close stdin/stdout/stderr and replace with /dev/null */
110 static int close_stdio(void) {
111         int fd;
112
113         if ((fd = open("/dev/null", O_RDWR)) == -1)
114                 return -1;
115
116         if (dup2(fd, 0) == -1 || dup2(fd, 1) == -1 || dup2(fd, 2) == -1)
117                 return -1;
118
119         close(fd);
120         return 0;
121 }
122
123 static void launch_tear(struct swb_context *ctx, char *uri) {
124         int status;
125         static DBusGProxy *tear_proxy = NULL;
126         GError *error = NULL;
127         pid_t pid;
128
129         if (!uri)
130                 uri = "new_window";
131
132         printf("launch_tear with uri '%s'\n", uri);
133
134         /* We should be able to just call the D-Bus service to open Tear ...
135            but if Tear's not open, that cuases D-Bus to start Tear and then
136            pass it the OpenAddress call, which results in two browser windows.
137            Properly fixing this probably requires Tear to provide a D-Bus
138            method that opens an address in an existing window, but for now work
139            around by just invoking Tear with exec() if it's not running. */
140         status = system("pidof tear > /dev/null");
141         if (WIFEXITED(status) && !WEXITSTATUS(status)) {
142                 if (!tear_proxy)
143                         tear_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
144                                         "com.nokia.tear", "/com/nokia/tear",
145                                         "com.nokia.Tear");
146                 dbus_g_proxy_call(tear_proxy, "OpenAddress", &error,
147                                   G_TYPE_STRING, uri, G_TYPE_INVALID);
148                 if (!ctx->continuous_mode)
149                         exit(0);
150         } else {
151                 if (ctx->continuous_mode) {
152                         if ((pid = fork()) != 0) {
153                                 /* Parent process or error in fork() */
154                                 printf("child: %d\n", (int)pid);
155                                 return;
156                         }
157                         /* Child process */
158                         setsid();
159                         close_stdio();
160                 }
161                 execl("/usr/bin/tear", "/usr/bin/tear", uri, (char *)NULL);
162         }
163 }
164
165 void launch_microb(struct swb_context *ctx, char *uri) {
166         int kill_browserd = 0;
167         int status;
168         pid_t pid;
169 #ifdef FREMANTLE
170         DBusConnection *raw_connection;
171         DBusError dbus_error;
172         DBusHandleMessageFunction filter_func;
173         DBusGProxy *g_proxy;
174         GError *gerror = NULL;
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         if (pid > 0) {
202                 /* Parent process */
203                 /* Wait for our child to start the browser UI process and
204                    for it to acquire the com.nokia.osso_browser D-Bus name,
205                    then make the appropriate method call to open the browser
206                    window.
207
208                    Ideas for how to do this monitoring derived from the
209                    dbus-monitor code (tools/dbus-monitor.c in the D-Bus
210                    codebase). */
211                 microb_started = 0;
212                 dbus_error_init(&dbus_error);
213
214                 raw_connection = dbus_bus_get_private(DBUS_BUS_SESSION,
215                                                       &dbus_error);
216                 if (!raw_connection) {
217                         fprintf(stderr,
218                                 "Failed to open connection to session bus: %s\n",
219                                 dbus_error.message);
220                         dbus_error_free(&dbus_error);
221                         exit(1);
222                 }
223
224                 dbus_bus_add_match(raw_connection,
225                                    "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
226                                    &dbus_error);
227                 if (dbus_error_is_set(&dbus_error)) {
228                         fprintf(stderr,
229                                 "Failed to set up watch for browser UI start: %s\n",
230                                 dbus_error.message);
231                         dbus_error_free(&dbus_error);
232                         exit(1);
233                 }
234                 filter_func = check_microb_started;
235                 if (!dbus_connection_add_filter(raw_connection,
236                                                 filter_func, NULL, NULL)) {
237                         fprintf(stderr, "Failed to set up watch filter!\n");
238                         exit(1);
239                 }
240                 printf("Waiting for MicroB to start\n");
241                 while (!microb_started &&
242                        dbus_connection_read_write_dispatch(raw_connection,
243                                                            -1));
244                 dbus_connection_remove_filter(raw_connection,
245                                               filter_func, NULL);
246                 dbus_bus_remove_match(raw_connection,
247                                       "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
248                                       &dbus_error);
249                 if (dbus_error_is_set(&dbus_error)) {
250                         fprintf(stderr,
251                                 "Failed to remove watch for browser UI start: %s\n",
252                                 dbus_error.message);
253                         dbus_error_free(&dbus_error);
254                         exit(1);
255                 }
256
257                 /* Browser UI's started, send it the request for a new window
258                    via D-Bus */
259                 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
260                                 "com.nokia.osso_browser",
261                                 "/com/nokia/osso_browser/request",
262                                 "com.nokia.osso_browser");
263                 if (!g_proxy) {
264                         printf("Couldn't get a com.nokia.osso_browser proxy\n");
265                         exit(1);
266                 }
267                 if (!strcmp(uri, "new_window")) {
268 #if 0 /* Since we can't detect when the bookmark window closes, we'd have a
269          corner case where, if the user just closes the bookmark window
270          without opening any browser windows, we don't kill off MicroB or
271          resume handling com.nokia.osso_browser */
272                         if (!dbus_g_proxy_call(g_proxy, "top_application",
273                                                &gerror, G_TYPE_INVALID,
274                                                G_TYPE_INVALID)) {
275                                 printf("Opening window failed: %s\n",
276                                        gerror->message);
277                                 exit(1);
278                         }
279 #endif
280                         if (!dbus_g_proxy_call(g_proxy, "load_url",
281                                                &gerror,
282                                                G_TYPE_STRING, "about:blank",
283                                                G_TYPE_INVALID,
284                                                G_TYPE_INVALID)) {
285                                 printf("Opening window failed: %s\n",
286                                        gerror->message);
287                                 exit(1);
288                         }
289                 } else {
290                         if (!dbus_g_proxy_call(g_proxy, "load_url",
291                                                &gerror,
292                                                G_TYPE_STRING, uri,
293                                                G_TYPE_INVALID,
294                                                G_TYPE_INVALID)) {
295                                 printf("Opening window failed: %s\n",
296                                        gerror->message);
297                                 exit(1);
298                         }
299                 }
300                 g_object_unref(g_proxy);
301
302                 /* Workaround: the browser process we started is going to want
303                    to hang around forever, hogging the com.nokia.osso_browser
304                    D-Bus interface while at it.  To fix this, we notice that
305                    when the last browser window closes, the browser UI restarts
306                    its attached browserd process, which causes an observable
307                    change in the ownership of the Mozilla.MicroB D-Bus name.
308                    Watch for this change and kill off the browser UI process
309                    when it happens.
310
311                    This has the problem of not being able to detect whether
312                    the bookmark window is open and/or in use, but it's the best
313                    that I can think of.  Better suggestions would be greatly
314                    appreciated. */
315                 kill_microb = 0;
316                 dbus_bus_add_match(raw_connection,
317                                    "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='Mozilla.MicroB'",
318                                    &dbus_error);
319                 if (dbus_error_is_set(&dbus_error)) {
320                         fprintf(stderr,
321                                 "Failed to set up watch for browserd restart: %s\n",
322                                 dbus_error.message);
323                         dbus_error_free(&dbus_error);
324                         exit(1);
325                 }
326                 /* Maemo 5 PR1.1 seems to have changed the name browserd takes
327                    to com.nokia.microb-engine; look for this too */
328                 dbus_bus_add_match(raw_connection,
329                                    "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.microb-engine'",
330                                    &dbus_error);
331                 if (dbus_error_is_set(&dbus_error)) {
332                         fprintf(stderr,
333                                 "Failed to set up watch for browserd restart: %s\n",
334                                 dbus_error.message);
335                         dbus_error_free(&dbus_error);
336                         exit(1);
337                 }
338                 filter_func = check_microb_finished;
339                 if (!dbus_connection_add_filter(raw_connection,
340                                                 filter_func, NULL, NULL)) {
341                         fprintf(stderr, "Failed to set up watch filter!\n");
342                         exit(1);
343                 }
344                 while (!kill_microb &&
345                        dbus_connection_read_write_dispatch(raw_connection,
346                                                            -1));
347                 dbus_connection_remove_filter(raw_connection,
348                                               filter_func, NULL);
349                 dbus_bus_remove_match(raw_connection,
350                                    "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='Mozilla.MicroB'",
351                                    &dbus_error);
352                 if (dbus_error_is_set(&dbus_error))
353                         /* Don't really care -- about to disconnect from the
354                            bus anyhow */
355                         dbus_error_free(&dbus_error);
356                 dbus_bus_remove_match(raw_connection,
357                                    "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.microb-engine'",
358                                    &dbus_error);
359                 if (dbus_error_is_set(&dbus_error))
360                         dbus_error_free(&dbus_error);
361                 dbus_connection_close(raw_connection);
362                 dbus_connection_unref(raw_connection);
363
364                 /* Kill off browser UI
365                    XXX: Hope we don't cause data loss here! */
366                 printf("Killing MicroB\n");
367                 kill(pid, SIGTERM);
368         } else {
369                 /* Child process */
370                 close_stdio();
371
372                 /* exec maemo-invoker directly instead of relying on the
373                    /usr/bin/browser symlink, since /usr/bin/browser may have
374                    been replaced with a shell script calling us via D-Bus */
375                 /* Launch the browser in the background -- our parent will
376                    wait for it to claim the D-Bus name and then display the
377                    window using D-Bus */
378                 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
379         }
380 #else /* !FREMANTLE */
381         if (pid > 0) {
382                 /* Parent process */
383                 waitpid(pid, &status, 0);
384         } else {
385                 /* Child process */
386                 close_stdio();
387
388                 /* exec maemo-invoker directly instead of relying on the
389                    /usr/bin/browser symlink, since /usr/bin/browser may have
390                    been replaced with a shell script calling us via D-Bus */
391                 if (!strcmp(uri, "new_window")) {
392                         execl("/usr/bin/maemo-invoker",
393                               "browser", (char *)NULL);
394                 } else {
395                         execl("/usr/bin/maemo-invoker",
396                               "browser", "--url", uri, (char *)NULL);
397                 }
398         }
399 #endif /* FREMANTLE */
400
401         /* Kill off browserd if we started it */
402         if (kill_browserd)
403                 system("kill `pidof browserd`");
404
405         if (!ctx || !ctx->continuous_mode) 
406                 exit(0);
407
408         dbus_request_osso_browser_name(ctx);
409 }
410
411 static void launch_other_browser(struct swb_context *ctx, char *uri) {
412         char *command;
413         char *quoted_uri, *quote;
414
415         size_t cmdlen, urilen;
416         size_t quoted_uri_size;
417         size_t offset;
418
419         if (!uri || !strcmp(uri, "new_window"))
420                 uri = "";
421
422         printf("launch_other_browser with uri '%s'\n", uri);
423
424         if ((urilen = strlen(uri)) > 0) {
425                 /* Quote the URI to prevent the shell from interpreting it */
426                 /* urilen+3 = length of URI + 2x \' + \0 */
427                 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
428                         exit(1);
429                 snprintf(quoted_uri, urilen+3, "'%s'", uri);
430
431                 /* If there are any 's in the original URI, URL-escape them
432                    (replace them with %27) */
433                 quoted_uri_size = urilen + 3;
434                 quote = quoted_uri + 1;
435                 while ((quote = strchr(quote, '\'')) &&
436                        (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
437                         /* Check to make sure we don't shrink the memory area
438                            as a result of integer overflow */
439                         if (quoted_uri_size+2 <= quoted_uri_size)
440                                 exit(1);
441
442                         /* Grow the memory area;
443                            2 = strlen("%27")-strlen("'") */
444                         if (!(quoted_uri = realloc(quoted_uri,
445                                                    quoted_uri_size+2)))
446                                 exit(1);
447                         quoted_uri_size = quoted_uri_size + 2;
448
449                         /* Recalculate the location of the ' character --
450                            realloc() may have moved the string in memory */
451                         quote = quoted_uri + offset;
452
453                         /* Move the string after the ', including the \0,
454                            over two chars */
455                         memmove(quote+3, quote+1, strlen(quote));
456                         memcpy(quote, "%27", 3);
457                         quote = quote + 3;
458                 }
459                 urilen = strlen(quoted_uri);
460         } else
461                 quoted_uri = uri;
462
463         cmdlen = strlen(ctx->other_browser_cmd);
464
465         /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
466            replace "%s"), but is needed in the case other_browser_cmd has no %s
467            and urilen < 2 */
468         if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
469                 exit(1);
470         snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
471         printf("command: '%s'\n", command);
472
473         if (ctx->continuous_mode) {
474                 if (fork() != 0) {
475                         /* Parent process or error in fork() */
476                         if (urilen > 0)
477                                 free(quoted_uri);
478                         free(command);  
479                         return;
480                 }
481                 /* Child process */
482                 setsid();
483                 close_stdio();
484         }
485         execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
486 }
487
488 /* Use launch_other_browser as the default browser launcher, with the string
489    passed in as the other_browser_cmd
490    Resulting other_browser_cmd is always safe to free(), even if a pointer
491    to a string constant is passed in */
492 static void use_other_browser_cmd(struct swb_context *ctx, char *cmd) {
493         size_t len = strlen(cmd);
494
495         free(ctx->other_browser_cmd);
496         ctx->other_browser_cmd = calloc(len+1, sizeof(char));
497         if (!ctx->other_browser_cmd) {
498                 printf("malloc failed!\n");
499                 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
500         } else {
501                 ctx->other_browser_cmd = strncpy(ctx->other_browser_cmd,
502                                                  cmd, len+1);
503                 ctx->default_browser_launcher = launch_other_browser;
504         }
505 }
506
507 void update_default_browser(struct swb_context *ctx, char *default_browser) {
508         if (!ctx)
509                 return;
510
511         if (!default_browser) {
512                 /* No default_browser configured -- use built-in default */
513                 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
514                 return;
515         }
516
517         if (!strcmp(default_browser, "tear"))
518                 ctx->default_browser_launcher = launch_tear;
519         else if (!strcmp(default_browser, "microb"))
520                 ctx->default_browser_launcher = launch_microb;
521         else if (!strcmp(default_browser, "fennec"))
522                 /* Cheat and reuse launch_other_browser, since we don't appear
523                    to need to do anything special */
524                 use_other_browser_cmd(ctx, "fennec %s");
525         else if (!strcmp(default_browser, "midori"))
526                 use_other_browser_cmd(ctx, "midori %s");
527         else if (!strcmp(default_browser, "other")) {
528                 if (ctx->other_browser_cmd)
529                         ctx->default_browser_launcher = launch_other_browser;
530                 else {
531                         printf("default_browser is 'other', but no other_browser_cmd set -- using default\n");
532                         ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
533                 }
534         } else {
535                 printf("Unknown default_browser %s, using default", default_browser);
536                 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
537         }
538 }
539
540 void launch_browser(struct swb_context *ctx, char *uri) {
541         if (ctx && ctx->default_browser_launcher)
542                 ctx->default_browser_launcher(ctx, uri);
543 }