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