Update Fremantle screenshot with new 3.1 UI
[browser-switch] / launcher.c
1 /*
2  * launcher.c -- functions for launching web browsers for browser-switchboard
3  *
4  * Copyright (C) 2009-2010 Steven Luo
5  * Derived from a Python implementation by Jason Simpson and Steven Luo
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
20  * USA.
21  */
22
23 #include <stdlib.h>
24 #include <string.h>
25 #include <stdio.h>
26 #include <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                         if (!(tear_proxy = dbus_g_proxy_new_for_name(
144                                                 ctx->session_bus,
145                                                 "com.nokia.tear",
146                                                 "/com/nokia/tear",
147                                                 "com.nokia.Tear"))) {
148                                 printf("Failed to create proxy for com.nokia.Tear D-Bus interface\n");
149                                 exit(1);
150                         }
151                 }
152
153                 if (!dbus_g_proxy_call(tear_proxy, "OpenAddress", &error,
154                                        G_TYPE_STRING, uri, G_TYPE_INVALID,
155                                        G_TYPE_INVALID)) {
156                         printf("Opening window failed: %s\n", error->message);
157                         exit(1);
158                 }
159                 if (!ctx->continuous_mode)
160                         exit(0);
161         } else {
162                 if (ctx->continuous_mode) {
163                         if ((pid = fork()) != 0) {
164                                 /* Parent process or error in fork() */
165                                 printf("child: %d\n", (int)pid);
166                                 return;
167                         }
168                         /* Child process */
169                         setsid();
170                         close_stdio();
171                 }
172                 execl("/usr/bin/tear", "/usr/bin/tear", uri, (char *)NULL);
173         }
174 }
175
176 void launch_microb(struct swb_context *ctx, char *uri) {
177         int kill_browserd = 0;
178         int status;
179         pid_t pid;
180 #ifdef FREMANTLE
181         DBusConnection *raw_connection;
182         DBusError dbus_error;
183         DBusHandleMessageFunction filter_func;
184         DBusGProxy *g_proxy;
185         GError *gerror = NULL;
186 #endif
187
188         if (!uri)
189                 uri = "new_window";
190
191         printf("launch_microb with uri '%s'\n", uri);
192
193         /* Launch browserd if it's not running */
194         status = system("pidof browserd > /dev/null");
195         if (WIFEXITED(status) && WEXITSTATUS(status)) {
196                 kill_browserd = 1;
197 #ifdef FREMANTLE
198                 system("/usr/sbin/browserd -d -b > /dev/null 2>&1");
199 #else
200                 system("/usr/sbin/browserd -d > /dev/null 2>&1");
201 #endif
202         }
203
204         /* Release the osso_browser D-Bus name so that MicroB can take it */
205         dbus_release_osso_browser_name(ctx);
206
207         if ((pid = fork()) == -1) {
208                 perror("fork");
209                 exit(1);
210         }
211 #ifdef FREMANTLE
212         if (pid > 0) {
213                 /* Parent process */
214                 /* Wait for our child to start the browser UI process and
215                    for it to acquire the com.nokia.osso_browser D-Bus name,
216                    then make the appropriate method call to open the browser
217                    window.
218
219                    Ideas for how to do this monitoring derived from the
220                    dbus-monitor code (tools/dbus-monitor.c in the D-Bus
221                    codebase). */
222                 microb_started = 0;
223                 dbus_error_init(&dbus_error);
224
225                 raw_connection = dbus_bus_get_private(DBUS_BUS_SESSION,
226                                                       &dbus_error);
227                 if (!raw_connection) {
228                         fprintf(stderr,
229                                 "Failed to open connection to session bus: %s\n",
230                                 dbus_error.message);
231                         dbus_error_free(&dbus_error);
232                         exit(1);
233                 }
234
235                 dbus_bus_add_match(raw_connection,
236                                    "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
237                                    &dbus_error);
238                 if (dbus_error_is_set(&dbus_error)) {
239                         fprintf(stderr,
240                                 "Failed to set up watch for browser UI start: %s\n",
241                                 dbus_error.message);
242                         dbus_error_free(&dbus_error);
243                         exit(1);
244                 }
245                 filter_func = check_microb_started;
246                 if (!dbus_connection_add_filter(raw_connection,
247                                                 filter_func, NULL, NULL)) {
248                         fprintf(stderr, "Failed to set up watch filter!\n");
249                         exit(1);
250                 }
251                 printf("Waiting for MicroB to start\n");
252                 while (!microb_started &&
253                        dbus_connection_read_write_dispatch(raw_connection,
254                                                            -1));
255                 dbus_connection_remove_filter(raw_connection,
256                                               filter_func, NULL);
257                 dbus_bus_remove_match(raw_connection,
258                                       "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.osso_browser'",
259                                       &dbus_error);
260                 if (dbus_error_is_set(&dbus_error)) {
261                         fprintf(stderr,
262                                 "Failed to remove watch for browser UI start: %s\n",
263                                 dbus_error.message);
264                         dbus_error_free(&dbus_error);
265                         exit(1);
266                 }
267
268                 /* Browser UI's started, send it the request for a new window
269                    via D-Bus */
270                 g_proxy = dbus_g_proxy_new_for_name(ctx->session_bus,
271                                 "com.nokia.osso_browser",
272                                 "/com/nokia/osso_browser/request",
273                                 "com.nokia.osso_browser");
274                 if (!g_proxy) {
275                         printf("Couldn't get a com.nokia.osso_browser proxy\n");
276                         exit(1);
277                 }
278                 if (!strcmp(uri, "new_window")) {
279 #if 0 /* Since we can't detect when the bookmark window closes, we'd have a
280          corner case where, if the user just closes the bookmark window
281          without opening any browser windows, we don't kill off MicroB or
282          resume handling com.nokia.osso_browser */
283                         if (!dbus_g_proxy_call(g_proxy, "top_application",
284                                                &gerror, G_TYPE_INVALID,
285                                                G_TYPE_INVALID)) {
286                                 printf("Opening window failed: %s\n",
287                                        gerror->message);
288                                 exit(1);
289                         }
290 #endif
291                         if (!dbus_g_proxy_call(g_proxy, "load_url",
292                                                &gerror,
293                                                G_TYPE_STRING, "about:blank",
294                                                G_TYPE_INVALID,
295                                                G_TYPE_INVALID)) {
296                                 printf("Opening window failed: %s\n",
297                                        gerror->message);
298                                 exit(1);
299                         }
300                 } else {
301                         if (!dbus_g_proxy_call(g_proxy, "load_url",
302                                                &gerror,
303                                                G_TYPE_STRING, uri,
304                                                G_TYPE_INVALID,
305                                                G_TYPE_INVALID)) {
306                                 printf("Opening window failed: %s\n",
307                                        gerror->message);
308                                 exit(1);
309                         }
310                 }
311                 g_object_unref(g_proxy);
312
313                 /* Workaround: the browser process we started is going to want
314                    to hang around forever, hogging the com.nokia.osso_browser
315                    D-Bus interface while at it.  To fix this, we notice that
316                    when the last browser window closes, the browser UI restarts
317                    its attached browserd process, which causes an observable
318                    change in the ownership of the Mozilla.MicroB D-Bus name.
319                    Watch for this change and kill off the browser UI process
320                    when it happens.
321
322                    This has the problem of not being able to detect whether
323                    the bookmark window is open and/or in use, but it's the best
324                    that I can think of.  Better suggestions would be greatly
325                    appreciated. */
326                 kill_microb = 0;
327                 dbus_bus_add_match(raw_connection,
328                                    "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='Mozilla.MicroB'",
329                                    &dbus_error);
330                 if (dbus_error_is_set(&dbus_error)) {
331                         fprintf(stderr,
332                                 "Failed to set up watch for browserd restart: %s\n",
333                                 dbus_error.message);
334                         dbus_error_free(&dbus_error);
335                         exit(1);
336                 }
337                 /* Maemo 5 PR1.1 seems to have changed the name browserd takes
338                    to com.nokia.microb-engine; look for this too */
339                 dbus_bus_add_match(raw_connection,
340                                    "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.microb-engine'",
341                                    &dbus_error);
342                 if (dbus_error_is_set(&dbus_error)) {
343                         fprintf(stderr,
344                                 "Failed to set up watch for browserd restart: %s\n",
345                                 dbus_error.message);
346                         dbus_error_free(&dbus_error);
347                         exit(1);
348                 }
349                 filter_func = check_microb_finished;
350                 if (!dbus_connection_add_filter(raw_connection,
351                                                 filter_func, NULL, NULL)) {
352                         fprintf(stderr, "Failed to set up watch filter!\n");
353                         exit(1);
354                 }
355                 while (!kill_microb &&
356                        dbus_connection_read_write_dispatch(raw_connection,
357                                                            -1));
358                 dbus_connection_remove_filter(raw_connection,
359                                               filter_func, NULL);
360                 dbus_bus_remove_match(raw_connection,
361                                    "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='Mozilla.MicroB'",
362                                    &dbus_error);
363                 if (dbus_error_is_set(&dbus_error))
364                         /* Don't really care -- about to disconnect from the
365                            bus anyhow */
366                         dbus_error_free(&dbus_error);
367                 dbus_bus_remove_match(raw_connection,
368                                    "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='com.nokia.microb-engine'",
369                                    &dbus_error);
370                 if (dbus_error_is_set(&dbus_error))
371                         dbus_error_free(&dbus_error);
372                 dbus_connection_close(raw_connection);
373                 dbus_connection_unref(raw_connection);
374
375                 /* Kill off browser UI
376                    XXX: Hope we don't cause data loss here! */
377                 printf("Killing MicroB\n");
378                 kill(pid, SIGTERM);
379         } else {
380                 /* Child process */
381                 close_stdio();
382
383                 /* exec maemo-invoker directly instead of relying on the
384                    /usr/bin/browser symlink, since /usr/bin/browser may have
385                    been replaced with a shell script calling us via D-Bus */
386                 /* Launch the browser in the background -- our parent will
387                    wait for it to claim the D-Bus name and then display the
388                    window using D-Bus */
389                 execl("/usr/bin/maemo-invoker", "browser", (char *)NULL);
390         }
391 #else /* !FREMANTLE */
392         if (pid > 0) {
393                 /* Parent process */
394                 waitpid(pid, &status, 0);
395         } else {
396                 /* Child process */
397                 close_stdio();
398
399                 /* exec maemo-invoker directly instead of relying on the
400                    /usr/bin/browser symlink, since /usr/bin/browser may have
401                    been replaced with a shell script calling us via D-Bus */
402                 if (!strcmp(uri, "new_window")) {
403                         execl("/usr/bin/maemo-invoker",
404                               "browser", (char *)NULL);
405                 } else {
406                         execl("/usr/bin/maemo-invoker",
407                               "browser", "--url", uri, (char *)NULL);
408                 }
409         }
410 #endif /* FREMANTLE */
411
412         /* Kill off browserd if we started it */
413         if (kill_browserd)
414                 system("kill `pidof browserd`");
415
416         if (!ctx || !ctx->continuous_mode) 
417                 exit(0);
418
419         dbus_request_osso_browser_name(ctx);
420 }
421
422 static void launch_other_browser(struct swb_context *ctx, char *uri) {
423         char *command;
424         char *quoted_uri, *quote;
425
426         size_t cmdlen, urilen;
427         size_t quoted_uri_size;
428         size_t offset;
429
430         if (!uri || !strcmp(uri, "new_window"))
431                 uri = "";
432
433         printf("launch_other_browser with uri '%s'\n", uri);
434
435         if ((urilen = strlen(uri)) > 0) {
436                 /* Quote the URI to prevent the shell from interpreting it */
437                 /* urilen+3 = length of URI + 2x \' + \0 */
438                 if (!(quoted_uri = calloc(urilen+3, sizeof(char))))
439                         exit(1);
440                 snprintf(quoted_uri, urilen+3, "'%s'", uri);
441
442                 /* If there are any 's in the original URI, URL-escape them
443                    (replace them with %27) */
444                 quoted_uri_size = urilen + 3;
445                 quote = quoted_uri + 1;
446                 while ((quote = strchr(quote, '\'')) &&
447                        (offset = quote-quoted_uri) < strlen(quoted_uri)-1) {
448                         /* Check to make sure we don't shrink the memory area
449                            as a result of integer overflow */
450                         if (quoted_uri_size+2 <= quoted_uri_size)
451                                 exit(1);
452
453                         /* Grow the memory area;
454                            2 = strlen("%27")-strlen("'") */
455                         if (!(quoted_uri = realloc(quoted_uri,
456                                                    quoted_uri_size+2)))
457                                 exit(1);
458                         quoted_uri_size = quoted_uri_size + 2;
459
460                         /* Recalculate the location of the ' character --
461                            realloc() may have moved the string in memory */
462                         quote = quoted_uri + offset;
463
464                         /* Move the string after the ', including the \0,
465                            over two chars */
466                         memmove(quote+3, quote+1, strlen(quote));
467                         memcpy(quote, "%27", 3);
468                         quote = quote + 3;
469                 }
470                 urilen = strlen(quoted_uri);
471         } else
472                 quoted_uri = uri;
473
474         cmdlen = strlen(ctx->other_browser_cmd);
475
476         /* cmdlen+urilen+1 is normally two bytes longer than we need (uri will
477            replace "%s"), but is needed in the case other_browser_cmd has no %s
478            and urilen < 2 */
479         if (!(command = calloc(cmdlen+urilen+1, sizeof(char))))
480                 exit(1);
481         snprintf(command, cmdlen+urilen+1, ctx->other_browser_cmd, quoted_uri);
482         printf("command: '%s'\n", command);
483
484         if (ctx->continuous_mode) {
485                 if (fork() != 0) {
486                         /* Parent process or error in fork() */
487                         if (urilen > 0)
488                                 free(quoted_uri);
489                         free(command);  
490                         return;
491                 }
492                 /* Child process */
493                 setsid();
494                 close_stdio();
495         }
496         execl("/bin/sh", "/bin/sh", "-c", command, (char *)NULL);
497 }
498
499 /* Use launch_other_browser as the default browser launcher, with the string
500    passed in as the other_browser_cmd
501    Resulting other_browser_cmd is always safe to free(), even if a pointer
502    to a string constant is passed in */
503 static void use_other_browser_cmd(struct swb_context *ctx, char *cmd) {
504         size_t len = strlen(cmd);
505
506         free(ctx->other_browser_cmd);
507         ctx->other_browser_cmd = calloc(len+1, sizeof(char));
508         if (!ctx->other_browser_cmd) {
509                 printf("malloc failed!\n");
510                 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
511         } else {
512                 ctx->other_browser_cmd = strncpy(ctx->other_browser_cmd,
513                                                  cmd, len+1);
514                 ctx->default_browser_launcher = launch_other_browser;
515         }
516 }
517
518 void update_default_browser(struct swb_context *ctx, char *default_browser) {
519         if (!ctx)
520                 return;
521
522         if (!default_browser) {
523                 /* No default_browser configured -- use built-in default */
524                 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
525                 return;
526         }
527
528         if (!strcmp(default_browser, "tear"))
529                 ctx->default_browser_launcher = launch_tear;
530         else if (!strcmp(default_browser, "microb"))
531                 ctx->default_browser_launcher = launch_microb;
532         else if (!strcmp(default_browser, "fennec"))
533                 /* Cheat and reuse launch_other_browser, since we don't appear
534                    to need to do anything special */
535                 use_other_browser_cmd(ctx, "fennec %s");
536         else if (!strcmp(default_browser, "midori"))
537                 use_other_browser_cmd(ctx, "midori %s");
538         else if (!strcmp(default_browser, "other")) {
539                 if (ctx->other_browser_cmd)
540                         ctx->default_browser_launcher = launch_other_browser;
541                 else {
542                         printf("default_browser is 'other', but no other_browser_cmd set -- using default\n");
543                         ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
544                 }
545         } else {
546                 printf("Unknown default_browser %s, using default", default_browser);
547                 ctx->default_browser_launcher = LAUNCH_DEFAULT_BROWSER;
548         }
549 }
550
551 void launch_browser(struct swb_context *ctx, char *uri) {
552         if (ctx && ctx->default_browser_launcher)
553                 ctx->default_browser_launcher(ctx, uri);
554 }