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