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