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